Decrypting Rich Resource Data From Teams Graph Change Notifications
Subscribe to Teams messages with includeResourceData, verify the dataSignature, and decrypt encrypted payloads with RSA-then-AES — the production details most tutorials skip.
- #microsoft-teams
- #ai
- #microsoft-graph
- #change-notifications
- #encryption
There are two ways to consume Teams message events from Microsoft Graph. The first is fetch-on-notify: Graph tells you “something changed at this resource,” and you call back to read it. The second is rich resource data, where Graph ships the changed payload inside the notification — encrypted, because chat content is sensitive and your notification endpoint is on the public internet. The second approach saves you a round trip per event and scales far better under load, but it asks you to handle a real cryptographic flow correctly. That flow is what trips people up, and it’s what this post walks through.
Why include resource data at all
If you subscribe to /teams/{id}/channels/{id}/messages and expect a hundred messages a minute, fetch-on-notify means a hundred extra Graph calls a minute just to read what changed — calls that count against your throttling budget and add latency. Setting includeResourceData: true on the subscription lets Graph deliver the message body in the notification itself. The cost is that you must provide an encryption certificate at subscription time, and you must verify and decrypt every payload you receive.
The subscription request carries your public cert:
{
"changeType": "created",
"resource": "/teams/{teamId}/channels/{channelId}/messages",
"notificationUrl": "https://your-endpoint.example.com/graph/notifications",
"includeResourceData": true,
"encryptionCertificate": "<base64 public cert>",
"encryptionCertificateId": "incident-cert-2026-06",
"expirationDateTime": "2026-06-24T00:00:00Z"
}
The encryptionCertificateId is a label you choose. It comes back on every notification so you know which private key to decrypt with — which is exactly what makes certificate rotation possible, as we’ll see.
Verify the signature before you trust anything
Each notification carries an encryptedContent object with three fields: data (the AES-encrypted payload), dataKey (the AES symmetric key, itself RSA-encrypted with your public cert), and dataSignature (an HMAC over the encrypted data). The single most important rule: verify the signature before you decrypt or act on anything.
The signature is an HMAC-SHA256 over the raw data value, using the decrypted symmetric key as the HMAC key. So the order is: decrypt the dataKey with your private cert to recover the symmetric key, compute the HMAC of data with that key, and compare it to dataSignature. If they don’t match, reject the notification — the payload was tampered with or spoofed, and nothing downstream should touch it.
import hmac, hashlib, base64
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
def decrypt_payload(encrypted, private_key):
# 1. RSA-OAEP unwrap the symmetric key
sym_key = private_key.decrypt(
base64.b64decode(encrypted["dataKey"]),
padding.OAEP(mgf=padding.MGF1(hashes.SHA1()),
algorithm=hashes.SHA1(), label=None),
)
# 2. Verify the HMAC signature BEFORE decrypting the body
expected = base64.b64encode(
hmac.new(sym_key, base64.b64decode(encrypted["data"]),
hashlib.sha256).digest()
).decode()
if not hmac.compare_digest(expected, encrypted["dataSignature"]):
raise ValueError("signature mismatch — reject this notification")
# 3. AES-CBC decrypt the payload (IV = first 16 bytes of the key)
iv = sym_key[:16]
cipher = Cipher(algorithms.AES(sym_key), modes.CBC(iv))
dec = cipher.decryptor()
raw = dec.update(base64.b64decode(encrypted["data"])) + dec.finalize()
return strip_pkcs7(raw)
A couple of details people get wrong here: the RSA unwrap uses OAEP padding, and the AES IV is the first 16 bytes of the symmetric key, not a separate field. Get either wrong and you’ll get garbage out with no obvious error. Verify against Microsoft’s current documentation when you implement this — the padding and IV conventions are exactly the kind of thing worth confirming against the source rather than trusting from memory.
Acknowledge fast, process async
Graph waits for your endpoint to return a 2xx within a few seconds. If you do your decryption, business logic, and downstream writes inline, you’ll blow that window under load, Graph will mark the delivery failed and retry, and now you’re double-processing. The pattern that survives traffic is: validate the subscription token (on creation), verify the signature, then immediately enqueue the encrypted-or-decrypted payload and return 202. A separate worker does the real work.
@app.post("/graph/notifications")
def notifications(req):
if "validationToken" in req.query: # subscription handshake
return Response(req.query["validationToken"], media_type="text/plain")
for note in req.json["value"]:
enqueue(note) # do not process inline
return Response(status_code=202)
This split is the difference between an endpoint that holds up at a thousand notifications a minute and one that collapses into a retry storm the first time traffic spikes.
Letting AI draft the receiver, then verifying
The crypto here is fiddly enough that I draft it with an AI assistant and then verify hard, because this is exactly the territory where a confident-sounding wrong answer (wrong padding, wrong IV, signature checked after decryption) creates a security hole rather than a crash.
Prompt: “Write a Python handler for Microsoft Graph change notifications with includeResourceData. Decrypt the dataKey with RSA-OAEP, verify the dataSignature HMAC-SHA256 before decrypting, then AES-CBC decrypt the body. Acknowledge with 202 and enqueue for async processing.”
Output (excerpt): A handler matching the structure above — RSA-OAEP unwrap, HMAC verification gating the AES decrypt, and a 202-and-enqueue shape — which I then diffed against Microsoft’s reference to confirm the padding scheme and IV derivation.
The model gets the overall flow right almost every time; what I verify is the cryptographic specifics and the signature-before-decrypt ordering. That’s the AI-drafts, human-verifies split that matters most when the cost of a subtle error is a spoofed payload getting processed.
Key rotation and missed events
Two operational realities close this out. First, certificates expire, so support multiple decryption certs keyed by encryptionCertificateId (which every notification echoes back). Keep the old key around until all subscriptions created under it have rolled over, and you can rotate without dropping in-flight notifications.
Second, change notifications are at-least-once and not a durable log. If your endpoint is down for ten minutes, those events are gone. Pair every subscription with a delta-query reconciliation sweep that catches up the gap when your receiver recovers — notifications give you low-latency events, delta gives you completeness, and you need both. The same logic applies to keeping the subscription itself alive; renewal and lifecycle handling are their own discipline.
Bringing it together
Verify the signature before you trust the payload, decrypt with RSA-then-AES in the right order, acknowledge within the timeout and process asynchronously, rotate certificates by id, and reconcile missed events with delta queries. For the broader event-driven patterns this sits inside, the Microsoft Teams category collects the related Graph automation work, and the prompts library has a receiver-and-decrypt prompt you can adapt so you’re not hand-writing the crypto flow from scratch. Get these pieces right and rich resource data turns a chatty fetch-on-notify integration into a lean, encrypted event pipeline that holds up under real traffic.
Download the Free 500-Prompt DevOps AI Toolkit
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.