website/docs/guides/microsoft-graph-app-registration.md
The Teams meeting pipeline reads meeting transcripts, recordings, and related artifacts from Microsoft Graph using app-only (daemon) authentication — no user sign-in, no interactive consent per meeting. That requires an Azure AD application registration with admin-consented application permissions.
This guide walks through:
You need tenant admin rights (or an admin to grant consent on your behalf) to finish this. Bookmark the values you collect — they go into ~/.hermes/.env at the end.
Hermes Teams Meeting Pipeline (or any name you'll recognize).You'll land on the app's overview page. Copy two values:
MSGRAPH_CLIENT_IDMSGRAPH_TENANT_IDhermes-graph-secret. Expires: pick a value that matches your rotation policy (6-24 months is typical).MSGRAPH_CLIENT_SECRET.The Secret ID column is not the secret. You want the Value column.
The pipeline uses a minimum-viable set of application permissions. Add only what you need; each one widens what the app can read tenant-wide.
<your tenant>. The Status column should flip to a green checkmark for every permission.| Permission | What it lets the app do |
|---|---|
OnlineMeetings.Read.All | Read Teams online meeting metadata (subject, participants, join URL). |
OnlineMeetingTranscript.Read.All | Read meeting transcripts generated by Teams. |
| Permission | What it lets the app do |
|---|---|
OnlineMeetingRecording.Read.All | Download Teams meeting recordings for offline STT processing. |
CallRecords.Read.All | Resolve meetings from call records when only the join URL is known. |
If platforms.teams.extra.delivery_mode is graph, the pipeline posts summaries into a Teams channel or chat via the Graph API. Skip these if you use incoming_webhook delivery mode instead.
| Permission | What it lets the app do |
|---|---|
ChannelMessage.Send | Post messages into Teams channels on behalf of the app. |
Chat.ReadWrite.All | Post messages into 1:1 and group chats (only if you set chat_id as the delivery target). |
OnlineMeetings.ReadWrite.All / Chat.ReadWrite without .All — broader than the pipeline needs.By default, application permissions like OnlineMeetings.Read.All grant the app access to every meeting in the tenant. For partner demos and dev tenants that's fine; for production you almost certainly want to restrict which users' meetings the app can read.
Microsoft provides Application Access Policies for Teams exactly for this. The policy is a PowerShell-only surface; there's no portal UI for it.
From an admin PowerShell with the MicrosoftTeams module installed and connected (Connect-MicrosoftTeams):
# Create a policy scoped to the Hermes app
New-CsApplicationAccessPolicy `
-Identity "Hermes-Meeting-Pipeline-Policy" `
-AppIds "<MSGRAPH_CLIENT_ID>" `
-Description "Restrict Hermes meeting pipeline to allow-listed users"
# Grant the policy to specific users whose meetings the pipeline may read
Grant-CsApplicationAccessPolicy `
-PolicyName "Hermes-Meeting-Pipeline-Policy" `
-Identity "[email protected]"
Grant-CsApplicationAccessPolicy `
-PolicyName "Hermes-Meeting-Pipeline-Policy" `
-Identity "[email protected]"
Propagation can take up to 30 minutes after granting. Verify with:
Test-CsApplicationAccessPolicy -Identity "[email protected]" -AppId "<MSGRAPH_CLIENT_ID>"
Without the policy, any user's meetings are readable — that's what the permission technically grants. Don't skip this step on a production tenant.
Put the three values you collected into ~/.hermes/.env:
MSGRAPH_TENANT_ID=<directory-tenant-id>
MSGRAPH_CLIENT_ID=<application-client-id>
MSGRAPH_CLIENT_SECRET=<client-secret-value>
Set file permissions so only you can read the secret:
chmod 600 ~/.hermes/.env
Hermes ships a Graph auth smoke-test. From your Hermes install:
python -c "
import asyncio
from tools.microsoft_graph_auth import MicrosoftGraphTokenProvider
provider = MicrosoftGraphTokenProvider.from_env()
token = asyncio.run(provider.get_access_token())
print('Token acquired, length:', len(token))
print(provider.inspect_token_health())
"
A successful run prints a long token string and a health dict showing cached: True and an expires_in_seconds value near 3600. Failures produce a MicrosoftGraphTokenError with the Azure error code — the most common are:
| Azure error | Meaning | Fix |
|---|---|---|
AADSTS7000215: Invalid client secret | Secret value mismatched or expired. | Generate a new secret in step 2; update .env. |
AADSTS700016: Application not found | Wrong MSGRAPH_CLIENT_ID or wrong tenant. | Double-check the values from step 1 are from the same app. |
AADSTS90002: Tenant not found | Typo in MSGRAPH_TENANT_ID. | Copy the Directory (tenant) ID from the app overview again. |
insufficient_claims at call time (not token time) | Token acquires but Graph returns 401/403. | You skipped step 3 admin-consent, or added permissions but haven't re-consented. Revisit API permissions and click Grant admin consent again. |
Azure client secrets have a hard expiry. Before yours expires:
MSGRAPH_CLIENT_SECRET in ~/.hermes/.env with the new value.hermes gateway restart.Once credentials verify cleanly, continue with:
msgraph_webhook gateway platform that receives Graph change notifications.Those pages land alongside the PRs that add the corresponding runtime. This credentials setup is a standalone prerequisite and is safe to complete in advance.