Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions apps/sim/app/api/webhooks/test/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,58 @@ export async function GET(request: NextRequest) {
})
}

case 'microsoftteams': {
const hmacSecret = providerConfig.hmacSecret

if (!hmacSecret) {
logger.warn(`[${requestId}] Microsoft Teams webhook missing HMAC secret: ${webhookId}`)
return NextResponse.json(
{ success: false, error: 'Microsoft Teams webhook requires HMAC secret' },
{ status: 400 }
)
}

logger.info(`[${requestId}] Microsoft Teams webhook test successful: ${webhookId}`)
return NextResponse.json({
success: true,
webhook: {
id: foundWebhook.id,
url: webhookUrl,
isActive: foundWebhook.isActive,
},
message: 'Microsoft Teams outgoing webhook configuration is valid.',
setup: {
url: webhookUrl,
hmacSecretConfigured: !!hmacSecret,
instructions: [
'Create an outgoing webhook in Microsoft Teams',
'Set the callback URL to the webhook URL above',
'Copy the HMAC security token to the configuration',
'Users can trigger the webhook by @mentioning it in Teams',
],
},
test: {
curlCommand: `curl -X POST "${webhookUrl}" \\
-H "Content-Type: application/json" \\
-H "Authorization: HMAC <signature>" \\
-d '{"type":"message","text":"Hello from Microsoft Teams!","from":{"id":"test","name":"Test User"}}'`,
samplePayload: {
type: 'message',
id: '1234567890',
timestamp: new Date().toISOString(),
text: 'Hello Sim Studio Bot!',
from: {
id: '29:1234567890abcdef',
name: 'Test User',
},
conversation: {
id: '19:meeting_abcdef@thread.v2',
},
},
},
})
}

default: {
// Generic webhook test
logger.info(`[${requestId}] Generic webhook test successful: ${webhookId}`)
Expand Down
46 changes: 46 additions & 0 deletions apps/sim/app/api/webhooks/trigger/[path]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
processGenericDeduplication,
processWebhook,
processWhatsAppDeduplication,
validateMicrosoftTeamsSignature,
} from '@/lib/webhooks/utils'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
import { db } from '@/db'
Expand Down Expand Up @@ -243,6 +244,51 @@ export async function POST(
return slackChallengeResponse
}

// Handle Microsoft Teams outgoing webhook signature verification (must be done before timeout)
if (foundWebhook.provider === 'microsoftteams') {
const providerConfig = (foundWebhook.providerConfig as Record<string, any>) || {}

if (providerConfig.hmacSecret) {
const authHeader = request.headers.get('authorization')

if (!authHeader || !authHeader.startsWith('HMAC ')) {
logger.warn(
`[${requestId}] Microsoft Teams outgoing webhook missing HMAC authorization header`
)
return new NextResponse('Unauthorized - Missing HMAC signature', { status: 401 })
}

// Get the raw body for HMAC verification
const rawBody = await request.text()

const isValidSignature = validateMicrosoftTeamsSignature(
providerConfig.hmacSecret,
authHeader,
rawBody
)

if (!isValidSignature) {
logger.warn(`[${requestId}] Microsoft Teams HMAC signature verification failed`)
return new NextResponse('Unauthorized - Invalid HMAC signature', { status: 401 })
}

logger.debug(`[${requestId}] Microsoft Teams HMAC signature verified successfully`)

// Parse the body again since we consumed it for verification
try {
body = JSON.parse(rawBody)
} catch (parseError) {
logger.error(
`[${requestId}] Failed to parse Microsoft Teams webhook body after verification`,
{
error: parseError instanceof Error ? parseError.message : String(parseError),
}
)
return new NextResponse('Invalid JSON payload', { status: 400 })
}
}
}

// Skip processing if another instance is already handling this request
if (!hasExecutionLock) {
logger.info(`[${requestId}] Skipping execution as lock was not acquired`)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { Shield, Terminal } from 'lucide-react'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { CodeBlock } from '@/components/ui/code-block'
import { Input } from '@/components/ui/input'
import { ConfigField } from '../ui/config-field'
import { ConfigSection } from '../ui/config-section'
import { InstructionsSection } from '../ui/instructions-section'
import { TestResultDisplay } from '../ui/test-result'

interface MicrosoftTeamsConfigProps {
hmacSecret: string
setHmacSecret: (secret: string) => void
isLoadingToken: boolean
testResult: {
success: boolean
message?: string
test?: any
Copy link
Contributor

Choose a reason for hiding this comment

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

style: test prop has type 'any'. Consider defining a specific type for test results

} | null
copied: string | null
copyToClipboard: (text: string, type: string) => void
testWebhook: () => Promise<void>
}

const teamsWebhookExample = JSON.stringify(
{
type: 'message',
id: '1234567890',
timestamp: '2023-01-01T00:00:00.000Z',
localTimestamp: '2023-01-01T00:00:00.000Z',
serviceUrl: 'https://smba.trafficmanager.net/amer/',
channelId: 'msteams',
from: {
id: '29:1234567890abcdef',
name: 'John Doe',
},
conversation: {
id: '19:meeting_abcdef@thread.v2',
},
text: 'Hello Sim Studio Bot!',
},
null,
2
)

export function MicrosoftTeamsConfig({
hmacSecret,
setHmacSecret,
isLoadingToken,
testResult,
copied,
copyToClipboard,
testWebhook,
}: MicrosoftTeamsConfigProps) {
return (
<div className='space-y-4'>
<ConfigSection title='Microsoft Teams Configuration'>
<ConfigField
id='teams-hmac-secret'
label='HMAC Secret'
description='The security token provided by Teams when creating an outgoing webhook. Used to verify request authenticity.'
>
<Input
id='teams-hmac-secret'
value={hmacSecret}
onChange={(e) => setHmacSecret(e.target.value)}
placeholder='Enter HMAC secret from Teams'
disabled={isLoadingToken}
type='password'
/>
</ConfigField>
</ConfigSection>

<TestResultDisplay
testResult={testResult}
copied={copied}
copyToClipboard={copyToClipboard}
showCurlCommand={true}
/>

<InstructionsSection
title='Setting up Outgoing Webhook in Microsoft Teams'
tip='Create an outgoing webhook in Teams to receive messages from Teams in Sim Studio.'
>
<ol className='list-inside list-decimal space-y-1'>
<li>Open Microsoft Teams and go to the team where you want to add the webhook.</li>
<li>Click the three dots (•••) next to the team name and select "Manage team".</li>
<li>Go to the "Apps" tab and click "Create an outgoing webhook".</li>
<li>Provide a name, description, and optionally a profile picture.</li>
<li>Set the callback URL to your Sim Studio webhook URL (shown above).</li>
<li>Copy the HMAC security token and paste it into the "HMAC Secret" field above.</li>
<li>Click "Create" to finish setup.</li>
</ol>
</InstructionsSection>

<InstructionsSection title='Receiving Messages from Teams'>
<p>
When users mention your webhook in Teams (using @mention), Teams will send a POST request
to your Sim Studio webhook URL with a payload like this:
</p>
<CodeBlock language='json' code={teamsWebhookExample} className='mt-2 text-sm' />
<ul className='mt-3 list-outside list-disc space-y-1 pl-4'>
<li>Messages are triggered by @mentioning the webhook name in Teams.</li>
<li>Requests include HMAC signature for authentication.</li>
<li>You have 5 seconds to respond to the webhook request.</li>
</ul>
</InstructionsSection>

<Alert>
<Shield className='h-4 w-4' />
<AlertTitle>Security</AlertTitle>
<AlertDescription>
The HMAC secret is used to verify that requests are actually coming from Microsoft Teams.
Keep it secure and never share it publicly.
</AlertDescription>
</Alert>

<Alert>
<Terminal className='h-4 w-4' />
<AlertTitle>Requirements</AlertTitle>
<AlertDescription>
<ul className='mt-1 list-outside list-disc space-y-1 pl-4'>
<li>Your Sim Studio webhook URL must use HTTPS and be publicly accessible.</li>
<li>Self-signed SSL certificates are not supported by Microsoft Teams.</li>
<li>For local testing, use a tunneling service like ngrok or Cloudflare Tunnel.</li>
</ul>
</AlertDescription>
</Alert>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { DiscordConfig } from './providers/discord'
import { GenericConfig } from './providers/generic'
import { GithubConfig } from './providers/github'
import { GmailConfig } from './providers/gmail'
import { MicrosoftTeamsConfig } from './providers/microsoftteams'
import { SlackConfig } from './providers/slack'
import { StripeConfig } from './providers/stripe'
import { TelegramConfig } from './providers/telegram'
Expand Down Expand Up @@ -79,6 +80,8 @@ export function WebhookModal({
const [discordAvatarUrl, setDiscordAvatarUrl] = useState('')
const [slackSigningSecret, setSlackSigningSecret] = useState('')
const [telegramBotToken, setTelegramBotToken] = useState('')
// Microsoft Teams-specific state
const [microsoftTeamsHmacSecret, setMicrosoftTeamsHmacSecret] = useState('')
// Airtable-specific state
const [airtableWebhookSecret, _setAirtableWebhookSecret] = useState('')
const [airtableBaseId, setAirtableBaseId] = useState('')
Expand All @@ -103,6 +106,7 @@ export function WebhookModal({
airtableTableId: '',
airtableIncludeCellValues: false,
telegramBotToken: '',
microsoftTeamsHmacSecret: '',
selectedLabels: ['INBOX'] as string[],
labelFilterBehavior: 'INCLUDE',
markAsRead: false,
Expand Down Expand Up @@ -259,6 +263,15 @@ export function WebhookModal({
includeRawEmail: config.includeRawEmail,
}))
}
} else if (webhookProvider === 'microsoftteams') {
const hmacSecret = config.hmacSecret || ''

setMicrosoftTeamsHmacSecret(hmacSecret)

setOriginalValues((prev) => ({
...prev,
microsoftTeamsHmacSecret: hmacSecret,
}))
}
}
}
Expand Down Expand Up @@ -303,7 +316,9 @@ export function WebhookModal({
!originalValues.selectedLabels.every((label) => selectedLabels.includes(label)) ||
labelFilterBehavior !== originalValues.labelFilterBehavior ||
markAsRead !== originalValues.markAsRead ||
includeRawEmail !== originalValues.includeRawEmail))
includeRawEmail !== originalValues.includeRawEmail)) ||
(webhookProvider === 'microsoftteams' &&
microsoftTeamsHmacSecret !== originalValues.microsoftTeamsHmacSecret)

setHasUnsavedChanges(hasChanges)
}, [
Expand All @@ -327,6 +342,7 @@ export function WebhookModal({
labelFilterBehavior,
markAsRead,
includeRawEmail,
microsoftTeamsHmacSecret,
])

// Validate required fields for current provider
Expand Down Expand Up @@ -354,6 +370,9 @@ export function WebhookModal({
case 'gmail':
isValid = selectedLabels.length > 0
break
case 'microsoftteams':
isValid = microsoftTeamsHmacSecret.trim() !== ''
break
}
setIsCurrentConfigValid(isValid)
}, [
Expand All @@ -364,6 +383,7 @@ export function WebhookModal({
whatsappVerificationToken,
telegramBotToken,
selectedLabels,
microsoftTeamsHmacSecret,
])

// Use the provided path or generate a UUID-based path
Expand Down Expand Up @@ -433,6 +453,10 @@ export function WebhookModal({
return {
botToken: telegramBotToken || undefined,
}
case 'microsoftteams':
return {
hmacSecret: microsoftTeamsHmacSecret,
}
default:
return {}
}
Expand Down Expand Up @@ -482,6 +506,7 @@ export function WebhookModal({
airtableTableId,
airtableIncludeCellValues,
telegramBotToken,
microsoftTeamsHmacSecret,
selectedLabels,
labelFilterBehavior,
markAsRead,
Expand Down Expand Up @@ -727,6 +752,18 @@ export function WebhookModal({
webhookUrl={webhookUrl}
/>
)
case 'microsoftteams':
return (
<MicrosoftTeamsConfig
hmacSecret={microsoftTeamsHmacSecret}
setHmacSecret={setMicrosoftTeamsHmacSecret}
isLoadingToken={isLoadingToken}
testResult={testResult}
copied={copied}
copyToClipboard={copyToClipboard}
testWebhook={testWebhook}
/>
)
default:
return (
<GenericConfig
Expand Down
Loading