1+ import { createHmac } from 'node:crypto'
12import { describe , expect , it } from 'vitest'
23import {
34 handleSlackChallenge ,
@@ -23,6 +24,66 @@ describe('slackHandler responses', () => {
2324 } )
2425} )
2526
27+ describe ( 'slackHandler request verification' , ( ) => {
28+ const rawBody = JSON . stringify ( { type : 'event_callback' } )
29+
30+ function signedRequest ( signingSecret : string , timestamp : string , body = rawBody ) : Request {
31+ const signature = createHmac ( 'sha256' , signingSecret )
32+ . update ( `v0:${ timestamp } :${ body } ` , 'utf8' )
33+ . digest ( 'hex' )
34+ return new Request ( 'https://sim.test/api/webhooks/trigger/slack' , {
35+ method : 'POST' ,
36+ headers : {
37+ 'x-slack-request-timestamp' : timestamp ,
38+ 'x-slack-signature' : `v0=${ signature } ` ,
39+ } ,
40+ } )
41+ }
42+
43+ function verify ( request : Request , providerConfig : Record < string , unknown > , body = rawBody ) {
44+ return slackHandler . verifyAuth ! ( {
45+ webhook : { } ,
46+ workflow : { } ,
47+ request : request as unknown as import ( 'next/server' ) . NextRequest ,
48+ rawBody : body ,
49+ requestId : 'slack-auth-test' ,
50+ providerConfig,
51+ } )
52+ }
53+
54+ it ( 'fails closed when a legacy Slack webhook has no signing secret' , async ( ) => {
55+ const response = await verify ( new Request ( 'https://sim.test' ) , { } )
56+
57+ expect ( response ?. status ) . toBe ( 401 )
58+ } )
59+
60+ it ( 'accepts a correctly signed current request' , async ( ) => {
61+ const signingSecret = 'test-signing-secret'
62+ const timestamp = String ( Math . floor ( Date . now ( ) / 1000 ) )
63+
64+ expect ( verify ( signedRequest ( signingSecret , timestamp ) , { signingSecret } ) ) . toBeNull ( )
65+ } )
66+
67+ it ( 'rejects a signature computed for different raw bytes' , async ( ) => {
68+ const signingSecret = 'test-signing-secret'
69+ const timestamp = String ( Math . floor ( Date . now ( ) / 1000 ) )
70+ const request = signedRequest ( signingSecret , timestamp )
71+
72+ const response = await verify ( request , { signingSecret } , `${ rawBody } ` )
73+
74+ expect ( response ?. status ) . toBe ( 401 )
75+ } )
76+
77+ it ( "rejects an otherwise valid signature outside Slack's five-minute replay window" , async ( ) => {
78+ const signingSecret = 'test-signing-secret'
79+ const timestamp = String ( Math . floor ( Date . now ( ) / 1000 ) - 301 )
80+
81+ const response = await verify ( signedRequest ( signingSecret , timestamp ) , { signingSecret } )
82+
83+ expect ( response ?. status ) . toBe ( 401 )
84+ } )
85+ } )
86+
2687describe ( 'slackHandler formatInput - Events API' , ( ) => {
2788 it ( 'maps an app_mention event' , async ( ) => {
2889 const { input } = await slackHandler . formatInput ! (
0 commit comments