Skip to content
🎉 Launch sale:50% off everything over $22 — automatically applied at checkout· ends Aug 2Shop the sale →
DevOps AI ToolKit
Newsletter
All guides
AI for Microsoft Teams By James Joyner IV · · 9 min read Last reviewed Jul 2026

Microsoft Teams Error Guide: Outgoing Webhook 'HMAC signature is not valid' — Verify the HMAC-SHA256

Quick answer

Fix Teams outgoing webhook 'HMAC signature is not valid': hash the raw body with the base64-decoded token, output base64, and compare in constant time.

  • #microsoft-teams
  • #adaptive-cards
  • #troubleshooting
  • #errors
Free toolkit

Stuck on this Microsoft Teams error? Get the free incident triage checklist

A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.

Overview

A Teams outgoing webhook signs every request to your endpoint with an HMAC in the Authorization header. When your service computes the HMAC differently than Teams did, verification fails and you must reject the call. The client shows a generic error and your logs record the mismatch.

HMAC signature is not valid.
expected: HMAC 9Qm5...=   computed: HMAC 1a4F...=
(rejecting request: 401 Unauthorized)

Teams sends the header as:

Authorization: HMAC 9Qm5xk2Yh0pQpS7l0m9m3Qk8b2m3Zk4b6Yh0pQpS7l=

Symptoms

  • Every message to your outgoing-webhook bot returns “Sorry, there was a problem” in Teams.
  • Your endpoint logs “HMAC signature is not valid” for legitimate Teams requests.
  • It works when you disable verification but you (correctly) don’t want to ship that.
  • Signatures match in a unit test with a fixed string but fail on live requests.
  • Started failing after adding a body parser or a proxy in front of the service.

Common Root Causes

  • Hashing the parsed body — computing HMAC over a re-serialized JSON object instead of the exact raw request bytes Teams sent.
  • Wrong key encoding — the security token is base64; you must decode it to bytes before using it as the HMAC key, not use the string directly.
  • Encoding drift — a middleware (body-parser, proxy, gzip) alters bytes/whitespace before you hash.
  • Wrong digest encoding — comparing hex vs base64, or forgetting the HMAC prefix.
  • Non-constant-time compare — using == (a correctness/security issue) or comparing mismatched formats.

Diagnostic Workflow

Capture the exact raw body and the header Teams sent, and compute the HMAC yourself. The key must be base64-decoded:

# SECURITY_TOKEN is the base64 token from the outgoing webhook setup.
# body.raw is the exact bytes of the request body.
printf '%s' "$(cat body.raw)" \
  | openssl dgst -sha256 -mac HMAC -macopt "hexkey:$(echo "$SECURITY_TOKEN" | base64 -d | xxd -p -c256)" -binary \
  | base64
# Compare this to the value after "HMAC " in the Authorization header.

Verification in code must hash the raw buffer, decode the key, and constant-time compare:

key   = base64Decode(SECURITY_TOKEN)          // bytes, not the string
mac   = HMAC_SHA256(key, rawRequestBody)       // raw bytes of the body
mine  = "HMAC " + base64Encode(mac)
ok    = constantTimeEqual(mine, authorizationHeader)

Make sure your framework hands you the raw body before JSON parsing:

# Express: capture raw body
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));
# Then HMAC req.rawBody, never JSON.stringify(req.body)

Reply within the expected window with a message activity:

{ "type": "message", "text": "Ack: rebooting node-7" }

Example Root Cause Analysis

An outgoing webhook routed slash-style commands from a Teams channel to an internal ops API. Verification failed for every real request but passed in tests. The tests hashed a fixed JSON string; production hashed JSON.stringify(req.body) after Express had already parsed and re-serialized the payload — reordering keys and changing whitespace, so the bytes no longer matched what Teams signed.

Two bugs, actually. Beyond hashing the re-serialized body, the code used the base64 security token as a string for the HMAC key instead of base64-decoding it to bytes. Capturing the raw body via the verify hook and decoding the key fixed both; the computed HMAC then matched the header on live traffic. They switched the comparison to a constant-time function to close the timing side-channel too.

Prevention Best Practices

  • HMAC the raw request bytes, captured before any JSON parsing or re-serialization.
  • Base64-decode the security token to bytes and use that as the HMAC-SHA256 key.
  • Emit the digest as base64 with the HMAC prefix to match Teams’ Authorization header format.
  • Compare using a constant-time function, never ==.
  • Ensure no proxy/middleware mutates the body (whitespace, gzip) before verification.
  • Rotate the security token periodically and store it in a secret manager, not in code.

Quick Command Reference

# Compute the expected HMAC over the raw body
printf '%s' "$(cat body.raw)" \
  | openssl dgst -sha256 -mac HMAC \
    -macopt "hexkey:$(echo "$TOKEN" | base64 -d | xxd -p -c256)" -binary | base64

# Confirm the token is valid base64
echo "$TOKEN" | base64 -d >/dev/null && echo "token decodes OK"

# Replay a captured request to your endpoint
curl -X POST https://ops.example.com/teams/webhook \
  -H "Authorization: HMAC $SIG" -H "Content-Type: application/json" \
  --data-binary @body.raw

Conclusion

“HMAC signature is not valid” means your computed digest doesn’t match the one Teams signed the request with — nearly always because you hashed a re-serialized body or used the base64 token as a raw string key. Capture the exact raw bytes, base64-decode the token into the key, emit a base64 digest with the HMAC prefix, and compare in constant time. Get those four right and your outgoing webhook authenticates every legitimate Teams callback while rejecting forged ones.

Free download · 368-page PDF

Fixed it? Get 500 Microsoft Teams & DevOps AI prompts — free

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.

Did this fix your issue?

Free download · 368-page PDF

Get 500 Battle-Tested DevOps AI Prompts — Free

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.