Skip to content

Commit 7739917

Browse files
working impl fo ms teams outgoing webhook (#740)
1 parent d94bfd9 commit 7739917

File tree

7 files changed

+399
-3
lines changed

7 files changed

+399
-3
lines changed

apps/sim/app/api/webhooks/test/route.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,58 @@ export async function GET(request: NextRequest) {
465465
})
466466
}
467467

468+
case 'microsoftteams': {
469+
const hmacSecret = providerConfig.hmacSecret
470+
471+
if (!hmacSecret) {
472+
logger.warn(`[${requestId}] Microsoft Teams webhook missing HMAC secret: ${webhookId}`)
473+
return NextResponse.json(
474+
{ success: false, error: 'Microsoft Teams webhook requires HMAC secret' },
475+
{ status: 400 }
476+
)
477+
}
478+
479+
logger.info(`[${requestId}] Microsoft Teams webhook test successful: ${webhookId}`)
480+
return NextResponse.json({
481+
success: true,
482+
webhook: {
483+
id: foundWebhook.id,
484+
url: webhookUrl,
485+
isActive: foundWebhook.isActive,
486+
},
487+
message: 'Microsoft Teams outgoing webhook configuration is valid.',
488+
setup: {
489+
url: webhookUrl,
490+
hmacSecretConfigured: !!hmacSecret,
491+
instructions: [
492+
'Create an outgoing webhook in Microsoft Teams',
493+
'Set the callback URL to the webhook URL above',
494+
'Copy the HMAC security token to the configuration',
495+
'Users can trigger the webhook by @mentioning it in Teams',
496+
],
497+
},
498+
test: {
499+
curlCommand: `curl -X POST "${webhookUrl}" \\
500+
-H "Content-Type: application/json" \\
501+
-H "Authorization: HMAC <signature>" \\
502+
-d '{"type":"message","text":"Hello from Microsoft Teams!","from":{"id":"test","name":"Test User"}}'`,
503+
samplePayload: {
504+
type: 'message',
505+
id: '1234567890',
506+
timestamp: new Date().toISOString(),
507+
text: 'Hello Sim Studio Bot!',
508+
from: {
509+
id: '29:1234567890abcdef',
510+
name: 'Test User',
511+
},
512+
conversation: {
513+
id: '19:meeting_abcdef@thread.v2',
514+
},
515+
},
516+
},
517+
})
518+
}
519+
468520
default: {
469521
// Generic webhook test
470522
logger.info(`[${requestId}] Generic webhook test successful: ${webhookId}`)

apps/sim/app/api/webhooks/trigger/[path]/route.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
processGenericDeduplication,
1212
processWebhook,
1313
processWhatsAppDeduplication,
14+
validateMicrosoftTeamsSignature,
1415
} from '@/lib/webhooks/utils'
1516
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
1617
import { db } from '@/db'
@@ -243,6 +244,51 @@ export async function POST(
243244
return slackChallengeResponse
244245
}
245246

247+
// Handle Microsoft Teams outgoing webhook signature verification (must be done before timeout)
248+
if (foundWebhook.provider === 'microsoftteams') {
249+
const providerConfig = (foundWebhook.providerConfig as Record<string, any>) || {}
250+
251+
if (providerConfig.hmacSecret) {
252+
const authHeader = request.headers.get('authorization')
253+
254+
if (!authHeader || !authHeader.startsWith('HMAC ')) {
255+
logger.warn(
256+
`[${requestId}] Microsoft Teams outgoing webhook missing HMAC authorization header`
257+
)
258+
return new NextResponse('Unauthorized - Missing HMAC signature', { status: 401 })
259+
}
260+
261+
// Get the raw body for HMAC verification
262+
const rawBody = await request.text()
263+
264+
const isValidSignature = validateMicrosoftTeamsSignature(
265+
providerConfig.hmacSecret,
266+
authHeader,
267+
rawBody
268+
)
269+
270+
if (!isValidSignature) {
271+
logger.warn(`[${requestId}] Microsoft Teams HMAC signature verification failed`)
272+
return new NextResponse('Unauthorized - Invalid HMAC signature', { status: 401 })
273+
}
274+
275+
logger.debug(`[${requestId}] Microsoft Teams HMAC signature verified successfully`)
276+
277+
// Parse the body again since we consumed it for verification
278+
try {
279+
body = JSON.parse(rawBody)
280+
} catch (parseError) {
281+
logger.error(
282+
`[${requestId}] Failed to parse Microsoft Teams webhook body after verification`,
283+
{
284+
error: parseError instanceof Error ? parseError.message : String(parseError),
285+
}
286+
)
287+
return new NextResponse('Invalid JSON payload', { status: 400 })
288+
}
289+
}
290+
}
291+
246292
// Skip processing if another instance is already handling this request
247293
if (!hasExecutionLock) {
248294
logger.info(`[${requestId}] Skipping execution as lock was not acquired`)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { Shield, Terminal } from 'lucide-react'
2+
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
3+
import { CodeBlock } from '@/components/ui/code-block'
4+
import { Input } from '@/components/ui/input'
5+
import { ConfigField } from '../ui/config-field'
6+
import { ConfigSection } from '../ui/config-section'
7+
import { InstructionsSection } from '../ui/instructions-section'
8+
import { TestResultDisplay } from '../ui/test-result'
9+
10+
interface MicrosoftTeamsConfigProps {
11+
hmacSecret: string
12+
setHmacSecret: (secret: string) => void
13+
isLoadingToken: boolean
14+
testResult: {
15+
success: boolean
16+
message?: string
17+
test?: any
18+
} | null
19+
copied: string | null
20+
copyToClipboard: (text: string, type: string) => void
21+
testWebhook: () => Promise<void>
22+
}
23+
24+
const teamsWebhookExample = JSON.stringify(
25+
{
26+
type: 'message',
27+
id: '1234567890',
28+
timestamp: '2023-01-01T00:00:00.000Z',
29+
localTimestamp: '2023-01-01T00:00:00.000Z',
30+
serviceUrl: 'https://smba.trafficmanager.net/amer/',
31+
channelId: 'msteams',
32+
from: {
33+
id: '29:1234567890abcdef',
34+
name: 'John Doe',
35+
},
36+
conversation: {
37+
id: '19:meeting_abcdef@thread.v2',
38+
},
39+
text: 'Hello Sim Studio Bot!',
40+
},
41+
null,
42+
2
43+
)
44+
45+
export function MicrosoftTeamsConfig({
46+
hmacSecret,
47+
setHmacSecret,
48+
isLoadingToken,
49+
testResult,
50+
copied,
51+
copyToClipboard,
52+
testWebhook,
53+
}: MicrosoftTeamsConfigProps) {
54+
return (
55+
<div className='space-y-4'>
56+
<ConfigSection title='Microsoft Teams Configuration'>
57+
<ConfigField
58+
id='teams-hmac-secret'
59+
label='HMAC Secret'
60+
description='The security token provided by Teams when creating an outgoing webhook. Used to verify request authenticity.'
61+
>
62+
<Input
63+
id='teams-hmac-secret'
64+
value={hmacSecret}
65+
onChange={(e) => setHmacSecret(e.target.value)}
66+
placeholder='Enter HMAC secret from Teams'
67+
disabled={isLoadingToken}
68+
type='password'
69+
/>
70+
</ConfigField>
71+
</ConfigSection>
72+
73+
<TestResultDisplay
74+
testResult={testResult}
75+
copied={copied}
76+
copyToClipboard={copyToClipboard}
77+
showCurlCommand={true}
78+
/>
79+
80+
<InstructionsSection
81+
title='Setting up Outgoing Webhook in Microsoft Teams'
82+
tip='Create an outgoing webhook in Teams to receive messages from Teams in Sim Studio.'
83+
>
84+
<ol className='list-inside list-decimal space-y-1'>
85+
<li>Open Microsoft Teams and go to the team where you want to add the webhook.</li>
86+
<li>Click the three dots (•••) next to the team name and select "Manage team".</li>
87+
<li>Go to the "Apps" tab and click "Create an outgoing webhook".</li>
88+
<li>Provide a name, description, and optionally a profile picture.</li>
89+
<li>Set the callback URL to your Sim Studio webhook URL (shown above).</li>
90+
<li>Copy the HMAC security token and paste it into the "HMAC Secret" field above.</li>
91+
<li>Click "Create" to finish setup.</li>
92+
</ol>
93+
</InstructionsSection>
94+
95+
<InstructionsSection title='Receiving Messages from Teams'>
96+
<p>
97+
When users mention your webhook in Teams (using @mention), Teams will send a POST request
98+
to your Sim Studio webhook URL with a payload like this:
99+
</p>
100+
<CodeBlock language='json' code={teamsWebhookExample} className='mt-2 text-sm' />
101+
<ul className='mt-3 list-outside list-disc space-y-1 pl-4'>
102+
<li>Messages are triggered by @mentioning the webhook name in Teams.</li>
103+
<li>Requests include HMAC signature for authentication.</li>
104+
<li>You have 5 seconds to respond to the webhook request.</li>
105+
</ul>
106+
</InstructionsSection>
107+
108+
<Alert>
109+
<Shield className='h-4 w-4' />
110+
<AlertTitle>Security</AlertTitle>
111+
<AlertDescription>
112+
The HMAC secret is used to verify that requests are actually coming from Microsoft Teams.
113+
Keep it secure and never share it publicly.
114+
</AlertDescription>
115+
</Alert>
116+
117+
<Alert>
118+
<Terminal className='h-4 w-4' />
119+
<AlertTitle>Requirements</AlertTitle>
120+
<AlertDescription>
121+
<ul className='mt-1 list-outside list-disc space-y-1 pl-4'>
122+
<li>Your Sim Studio webhook URL must use HTTPS and be publicly accessible.</li>
123+
<li>Self-signed SSL certificates are not supported by Microsoft Teams.</li>
124+
<li>For local testing, use a tunneling service like ngrok or Cloudflare Tunnel.</li>
125+
</ul>
126+
</AlertDescription>
127+
</Alert>
128+
</div>
129+
)
130+
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/webhook-modal.tsx

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { DiscordConfig } from './providers/discord'
1515
import { GenericConfig } from './providers/generic'
1616
import { GithubConfig } from './providers/github'
1717
import { GmailConfig } from './providers/gmail'
18+
import { MicrosoftTeamsConfig } from './providers/microsoftteams'
1819
import { SlackConfig } from './providers/slack'
1920
import { StripeConfig } from './providers/stripe'
2021
import { TelegramConfig } from './providers/telegram'
@@ -79,6 +80,8 @@ export function WebhookModal({
7980
const [discordAvatarUrl, setDiscordAvatarUrl] = useState('')
8081
const [slackSigningSecret, setSlackSigningSecret] = useState('')
8182
const [telegramBotToken, setTelegramBotToken] = useState('')
83+
// Microsoft Teams-specific state
84+
const [microsoftTeamsHmacSecret, setMicrosoftTeamsHmacSecret] = useState('')
8285
// Airtable-specific state
8386
const [airtableWebhookSecret, _setAirtableWebhookSecret] = useState('')
8487
const [airtableBaseId, setAirtableBaseId] = useState('')
@@ -103,6 +106,7 @@ export function WebhookModal({
103106
airtableTableId: '',
104107
airtableIncludeCellValues: false,
105108
telegramBotToken: '',
109+
microsoftTeamsHmacSecret: '',
106110
selectedLabels: ['INBOX'] as string[],
107111
labelFilterBehavior: 'INCLUDE',
108112
markAsRead: false,
@@ -259,6 +263,15 @@ export function WebhookModal({
259263
includeRawEmail: config.includeRawEmail,
260264
}))
261265
}
266+
} else if (webhookProvider === 'microsoftteams') {
267+
const hmacSecret = config.hmacSecret || ''
268+
269+
setMicrosoftTeamsHmacSecret(hmacSecret)
270+
271+
setOriginalValues((prev) => ({
272+
...prev,
273+
microsoftTeamsHmacSecret: hmacSecret,
274+
}))
262275
}
263276
}
264277
}
@@ -303,7 +316,9 @@ export function WebhookModal({
303316
!originalValues.selectedLabels.every((label) => selectedLabels.includes(label)) ||
304317
labelFilterBehavior !== originalValues.labelFilterBehavior ||
305318
markAsRead !== originalValues.markAsRead ||
306-
includeRawEmail !== originalValues.includeRawEmail))
319+
includeRawEmail !== originalValues.includeRawEmail)) ||
320+
(webhookProvider === 'microsoftteams' &&
321+
microsoftTeamsHmacSecret !== originalValues.microsoftTeamsHmacSecret)
307322

308323
setHasUnsavedChanges(hasChanges)
309324
}, [
@@ -327,6 +342,7 @@ export function WebhookModal({
327342
labelFilterBehavior,
328343
markAsRead,
329344
includeRawEmail,
345+
microsoftTeamsHmacSecret,
330346
])
331347

332348
// Validate required fields for current provider
@@ -354,6 +370,9 @@ export function WebhookModal({
354370
case 'gmail':
355371
isValid = selectedLabels.length > 0
356372
break
373+
case 'microsoftteams':
374+
isValid = microsoftTeamsHmacSecret.trim() !== ''
375+
break
357376
}
358377
setIsCurrentConfigValid(isValid)
359378
}, [
@@ -364,6 +383,7 @@ export function WebhookModal({
364383
whatsappVerificationToken,
365384
telegramBotToken,
366385
selectedLabels,
386+
microsoftTeamsHmacSecret,
367387
])
368388

369389
// Use the provided path or generate a UUID-based path
@@ -433,6 +453,10 @@ export function WebhookModal({
433453
return {
434454
botToken: telegramBotToken || undefined,
435455
}
456+
case 'microsoftteams':
457+
return {
458+
hmacSecret: microsoftTeamsHmacSecret,
459+
}
436460
default:
437461
return {}
438462
}
@@ -482,6 +506,7 @@ export function WebhookModal({
482506
airtableTableId,
483507
airtableIncludeCellValues,
484508
telegramBotToken,
509+
microsoftTeamsHmacSecret,
485510
selectedLabels,
486511
labelFilterBehavior,
487512
markAsRead,
@@ -727,6 +752,18 @@ export function WebhookModal({
727752
webhookUrl={webhookUrl}
728753
/>
729754
)
755+
case 'microsoftteams':
756+
return (
757+
<MicrosoftTeamsConfig
758+
hmacSecret={microsoftTeamsHmacSecret}
759+
setHmacSecret={setMicrosoftTeamsHmacSecret}
760+
isLoadingToken={isLoadingToken}
761+
testResult={testResult}
762+
copied={copied}
763+
copyToClipboard={copyToClipboard}
764+
testWebhook={testWebhook}
765+
/>
766+
)
730767
default:
731768
return (
732769
<GenericConfig

0 commit comments

Comments
 (0)