You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Golang+GAE配置MS Teams出站Webhook时HMAC验证失败求助

MS Teams出站Webhook HMAC验证失败问题排查与修复

问题描述

使用Golang结合Google App Engine部署MS Teams出站Webhook时,HMAC验证始终不通过,预期签名与请求提供的签名不一致。

原始代码

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "strings"
)

var secret string = "THIS_IS_A_SECRET"

func handleWebhook(w http.ResponseWriter, r *http.Request) {
    fmt.Println("Webhook endpoint hit")
    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Can't read request body", http.StatusBadRequest)
        return
    }
    fmt.Println("Received webhook request:", string(body))

    // Log the request body for debugging
    fmt.Printf("Request Body: %s\n", string(body))

    // Generate HMAC token from the request body
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expectedMAC := mac.Sum(nil)
    expectedMACBase64 := base64.StdEncoding.EncodeToString(expectedMAC)

    // Get the HMAC token from the request header
    authHeader := r.Header.Get("Authorization")
    if !strings.HasPrefix(authHeader, "HMAC ") {
        fmt.Println("Invalid Authorization header")
        http.Error(w, "Invalid Authorization header", http.StatusUnauthorized)
        return
    }
    providedMACBase64 := strings.TrimPrefix(authHeader, "HMAC ")

    // Compare the generated HMAC token with the provided one
    if !hmac.Equal([]byte(providedMACBase64), []byte(expectedMACBase64)) {
        fmt.Println("Invalid HMAC token")
        fmt.Println("Expected HMAC token:", expectedMACBase64)
        fmt.Println("Provided HMAC token:", providedMACBase64)
        http.Error(w, "Invalid HMAC token", http.StatusUnauthorized)
        return
    } else {
        fmt.Println("Authenticated: Valid HMAC token")
    }

    // Create a response in the format expected by Microsoft Teams
    response := map[string]string{
        "type": "message",
        "text": "Webhook received successfully",
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)

    json.NewEncoder(w).Encode(response)
}

func main() {
    http.HandleFunc("/teams-webhook", handleWebhook)
    http.ListenAndServe(":8080", nil)
}

问题根源

根据微软Teams官方文档的HMAC验证规则,核心错误为:

  • 密钥未正确解码:Teams提供的出站Webhook密钥是Base64编码格式的字符串,必须先解码为原始字节数组才能作为HMAC的密钥,代码中直接将原始字符串转为字节数组使用,导致密钥不匹配。
  • (次要可能)请求体被中间件修改:若GAE或其他中间件对请求体进行自动解析、编码转换(如UTF-8转码、添加额外字符),也会导致HMAC计算结果不一致,但优先解决密钥解码问题即可覆盖绝大多数场景。

修复后的代码

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "strings"
)

// Teams提供的Base64编码格式的密钥
var secret string = "THIS_IS_A_SECRET"

func handleWebhook(w http.ResponseWriter, r *http.Request) {
    fmt.Println("Webhook endpoint hit")
    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Can't read request body", http.StatusBadRequest)
        return
    }
    // 重置请求体,方便后续可能的二次读取(可选)
    r.Body = io.NopCloser(strings.NewReader(string(body)))
    fmt.Println("Received webhook request:", string(body))

    // 1. 对Teams提供的密钥进行Base64解码
    decodedSecret, err := base64.StdEncoding.DecodeString(secret)
    if err != nil {
        fmt.Println("Failed to decode secret:", err)
        http.Error(w, "Internal server error", http.StatusInternalServerError)
        return
    }

    // 2. 使用解码后的密钥计算HMAC-SHA256
    mac := hmac.New(sha256.New, decodedSecret)
    mac.Write(body)
    expectedMAC := mac.Sum(nil)
    expectedMACBase64 := base64.StdEncoding.EncodeToString(expectedMAC)

    // 获取请求头中的签名
    authHeader := r.Header.Get("Authorization")
    if !strings.HasPrefix(authHeader, "HMAC ") {
        fmt.Println("Invalid Authorization header")
        http.Error(w, "Invalid Authorization header", http.StatusUnauthorized)
        return
    }
    providedMACBase64 := strings.TrimPrefix(authHeader, "HMAC ")

    // 安全对比两个Base64字符串(使用hmac.Equal避免时序攻击)
    if !hmac.Equal([]byte(providedMACBase64), []byte(expectedMACBase64)) {
        fmt.Println("Invalid HMAC token")
        fmt.Println("Expected HMAC token:", expectedMACBase64)
        fmt.Println("Provided HMAC token:", providedMACBase64)
        http.Error(w, "Invalid HMAC token", http.StatusUnauthorized)
        return
    } else {
        fmt.Println("Authenticated: Valid HMAC token")
    }

    // 返回Teams预期格式的响应
    response := map[string]string{
        "type": "message",
        "text": "Webhook received successfully",
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(response)
}

func main() {
    http.HandleFunc("/teams-webhook", handleWebhook)
    http.ListenAndServe(":8080", nil)
}

关键修改说明

  • 新增base64.StdEncoding.DecodeString(secret)步骤,将Teams提供的Base64编码密钥解码为原始字节数组,这是验证通过的核心前提。
  • 添加请求体重置逻辑r.Body = io.NopCloser(strings.NewReader(string(body))),避免后续需要再次读取请求体时出现错误。
  • 保留hmac.Equal的安全对比方式,避免时序攻击风险。

内容的提问来源于stack exchange,提问作者TaihouKai

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 00:45:56