feat: zero-trust HMAC auth for Google Calendar webhook#690
feat: zero-trust HMAC auth for Google Calendar webhook#6902witstudios merged 2 commits intomasterfrom
Conversation
Remove the DB channel lookup fallback path so all sync-triggering webhook requests require a valid HMAC token. Fail-closed in production when OAUTH_STATE_SECRET is not configured. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📝 WalkthroughWalkthroughThis pull request implements zero-trust authentication for Google Calendar webhooks. A new authentication module validates incoming webhook requests using cryptographically signed tokens, replacing previous fallback logic. The webhook route handler was refactored to use this authentication system as the single source of truth. Comprehensive test suites were added for all new functionality. Changes
Sequence DiagramsequenceDiagram
actor Client
participant Route as Webhook Route
participant Auth as validateWebhookAuth
participant Token as verifyWebhookToken
participant Sync as Sync Service
Client->>Route: POST /webhook<br/>(X-Goog-Channel-Token)
alt Missing Headers
Route->>Client: 400 Missing headers
else resourceState=sync
Route->>Client: 200 ok=true
else Normal Request
Route->>Auth: validateWebhookAuth(channelToken)
alt Missing OAUTH_STATE_SECRET (Production)
Auth->>Client: 500 Internal Server Error
else Missing OAUTH_STATE_SECRET (Dev)
Auth->>Client: 401 Unauthorized
else Token Present
Auth->>Token: verifyWebhookToken(token)
alt Valid Token
Token-->>Auth: userId
Auth-->>Route: { userId }
Route->>Sync: Trigger sync for userId
Route->>Client: 200 OK
else Invalid/Tampered Token
Token-->>Auth: null
Auth->>Client: 401 Unauthorized
end
else Missing Token
Auth->>Client: 401 Unauthorized
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (5)
apps/web/src/lib/integrations/google-calendar/webhook-token.ts (1)
19-36: Fail-closed in production is correct; minor concern about the userId-dot contract.The token format
userId.signaturerelies on the invariant thatuserIdnever contains a dot. This is currently true for UUIDs and is noted in tests, but there's no runtime guard. If a non-UUID userId is ever passed,generateWebhookTokenwill produce a token thatverifyWebhookTokensilently rejects (sinceindexOf('.')would split at the wrong position).Consider adding a defensive check:
🛡️ Optional: guard against dots in userId
export const generateWebhookToken = (userId: string): string => { const secret = process.env.OAUTH_STATE_SECRET; if (!secret) { if (process.env.NODE_ENV === 'production') { throw new Error('OAUTH_STATE_SECRET must be configured in production'); } // Development: return empty string, webhook registration proceeds but auth will fail return ''; } + if (userId.includes('.')) { + throw new Error('userId must not contain dots (used as token delimiter)'); + } + const signature = cryptoapps/web/src/lib/integrations/google-calendar/webhook-auth.ts (1)
16-16: Consider makingWebhookAuthResultextensible or usingReadonly.Nit: wrapping the type in
Readonlysignals that the caller should not mutate the auth result. This is purely a defensive style choice.Optional readonly type
-export type WebhookAuthResult = { userId: string }; +export type WebhookAuthResult = Readonly<{ userId: string }>;apps/web/src/lib/integrations/google-calendar/__tests__/webhook-auth.test.ts (1)
106-116: InnerafterEachNODE_ENV restore is redundant (but harmless).The outer
afterEach(Line 15–17) replacesprocess.enventirely with the original reference, so the innerafterEachrestoringNODE_ENVon the clone (Line 114–116) has no lasting effect. Not a problem — just noting for clarity.apps/web/src/lib/integrations/google-calendar/__tests__/webhook-token.test.ts (1)
111-113: Nit: The non-hex test relies onBuffer.frombehavior — might be worth a brief inline comment.
Buffer.from('zzz...', 'hex')silently returns an empty buffer for invalid hex characters (rather than throwing), which triggers the length mismatch inverifyWebhookToken. This is the intended behavior but could be non-obvious to future readers. A one-liner comment in the test would help.apps/web/src/app/api/integrations/google-calendar/webhook/__tests__/route.test.ts (1)
46-71:createWebhookRequesthelper is well-designed; note the default-value asymmetry.
channelIdandresourceIddefault to non-null values when omitted, butchannelTokendefaults to omitted (no header set). This is intentional — most tests need valid IDs but want explicit control over the token — but it could surprise a future contributor. A brief comment on the helper would help.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
OAUTH_STATE_SECRETis not configured (500 instead of silent pass)webhook-auth.tsmodule with proper error responsesTest plan
webhook-token,webhook-auth,route)OAUTH_STATE_SECRETconfigured before deploy🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests