Skip to content

feat: zero-trust HMAC auth for Google Calendar webhook#690

Merged
2witstudios merged 2 commits intomasterfrom
feat/zero-trust-calendar-webhook
Feb 16, 2026
Merged

feat: zero-trust HMAC auth for Google Calendar webhook#690
2witstudios merged 2 commits intomasterfrom
feat/zero-trust-calendar-webhook

Conversation

@2witstudios
Copy link
Owner

@2witstudios 2witstudios commented Feb 16, 2026

Summary

  • Remove the DB channel lookup fallback so all sync-triggering webhook requests require a valid HMAC token (zero-trust, no fallback paths)
  • Fail-closed in production when OAUTH_STATE_SECRET is not configured (500 instead of silent pass)
  • Extract authentication logic into dedicated webhook-auth.ts module with proper error responses
  • Add 41 tests covering token generation/verification, auth validation, route integration, and critical no-fallback invariants

Test plan

  • All 41 webhook tests pass (webhook-token, webhook-auth, route)
  • Verify existing calendar webhook subscriptions re-register with HMAC tokens on next sync cycle
  • Confirm production has OAUTH_STATE_SECRET configured before deploy

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved error handling for Google Calendar webhooks, now returning explicit 500 errors instead of masking failures.
  • Tests

    • Added comprehensive test suites for webhook authentication, token generation and validation, and webhook routing to ensure reliability.

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>
@chatgpt-codex-connector
Copy link

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 16, 2026

Warning

Rate limit exceeded

@2witstudios has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 43 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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.

📝 Walkthrough

Walkthrough

This 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

Cohort / File(s) Summary
Webhook Authentication System
apps/web/src/lib/integrations/google-calendar/webhook-token.ts, apps/web/src/lib/integrations/google-calendar/webhook-auth.ts
New zero-trust authentication module and token validation. Modified token generation to throw error in production when OAUTH_STATE_SECRET is missing; fail-closed behavior with one-time warning in non-production.
Webhook Authentication Tests
apps/web/src/lib/integrations/google-calendar/__tests__/webhook-token.test.ts, apps/web/src/lib/integrations/google-calendar/__tests__/webhook-auth.test.ts
Comprehensive test suites validating token generation/verification and authentication flow, including fail-closed scenarios and missing secret configurations.
Webhook Route Handler
apps/web/src/app/api/integrations/google-calendar/webhook/route.ts
Refactored to use validateWebhookAuth as single source of truth, removing previous channel/resource ID fallback logic. Enhanced error handling returns 500 on unexpected errors instead of masking with 200.
Route Handler Tests
apps/web/src/app/api/integrations/google-calendar/webhook/__tests__/route.test.ts
Comprehensive test suite for webhook HTTP route covering header validation, sync confirmation, zero-trust authentication paths, fail-closed behavior, and logging.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A token secured with cryptographic care,
Zero-trust webhooks flourish in the air,
No fallback schemes, just auth that's true,
Google Calendar sync—now safer for you!
Tests cover every path, left and right, 🔐

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: zero-trust HMAC auth for Google Calendar webhook' directly and accurately summarizes the main architectural change: implementing zero-trust authentication with HMAC for Google Calendar webhooks, removing fallback paths.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into master

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/zero-trust-calendar-webhook

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.

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 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.signature relies on the invariant that userId never 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, generateWebhookToken will produce a token that verifyWebhookToken silently rejects (since indexOf('.') 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 = crypto
apps/web/src/lib/integrations/google-calendar/webhook-auth.ts (1)

16-16: Consider making WebhookAuthResult extensible or using Readonly.

Nit: wrapping the type in Readonly signals 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: Inner afterEach NODE_ENV restore is redundant (but harmless).

The outer afterEach (Line 15–17) replaces process.env entirely with the original reference, so the inner afterEach restoring NODE_ENV on 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 on Buffer.from behavior — 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 in verifyWebhookToken. 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: createWebhookRequest helper is well-designed; note the default-value asymmetry.

channelId and resourceId default to non-null values when omitted, but channelToken defaults 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>
@2witstudios 2witstudios merged commit 1f777e0 into master Feb 16, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant