2025-08-09
Build App-Only Microsoft Graph Integrations

TL;DR
-
Root cause of flaky automations: delegated permissions tied to human sessions.
-
Solution: App-only (application) permissions with a service principal → headless, least-privilege, auditable.
-
Outcome: Fewer auth tickets, safer scope, stable night runs, cleaner audits, easier scale.
Why delegated permissions keep breaking you
-
Tied to a user: password resets, MFA, role/licensing changes, offboarding.
-
Encourages over-privileged “service” users to stop errors → risky & messy.
-
Silent failures overnight, “insufficient privileges,” and brittle background jobs.
The smarter path: app-only with Microsoft Graph
-
The app (service principal) is the identity—no interactive login, no session to expire.
-
Least privilege: grant only what the automation needs (per resource/scope).
-
Auditability: actions attributed to the app; simpler compliance/storytelling.
Step-by-step setup (no gotchas)
-
Register app (Azure AD → App registrations).
-
Choose “Application” permissions (not Delegated) for Graph scopes you truly need (e.g.,
Sites.ReadWrite.All,Mail.Send,ChannelMessage.Send). -
Admin consent: Global/Privileged admin must grant tenant consent (verify in Enterprise apps → Permissions).
-
Credentials: Prefer certificates over client secrets; if using secrets, store in Azure Key Vault and rotate.
-
Tenant objects: Validate target site IDs, team/channel IDs, mailbox scopes; align permission to resource.
-
Error handling: Map 403/401 responses to missing scopes vs. missing consent; add retries with backoff; log correlation IDs.
Minimum permission recipes (examples)
-
SharePoint file bot:
Sites.Selected(best) + per-site consent, orSites.ReadWrite.Allif you must. -
Teams notifier:
ChannelMessage.Send(app) for targeted posting. -
Mail alerts:
Mail.Send(app) on a dedicated mailbox. -
User provisioning helpers: use narrowly scoped
User.ReadWrite.Allonly if required; prefer Graph lifecycle APIs.
Post-import binding (Power Platform tie-in)
-
After solution import, bind connection references to your app-only connection.
-
Use PAC CLI/PowerShell to set environment variables for tenant IDs, site IDs, channel IDs.
Real-time + reliability
-
Graph webhooks (SharePoint/Teams/Outlook): app-only subscriptions, endpoint validation, renew before expiry.
-
Implement idempotent handlers, DLQ/retry, and rate-limit guards.
-
Centralized logging (App Insights/Log Analytics) + alerting (failure rate, 401/403 spikes).
Security hardening checklist
-
Least privilege → prefer Sites.Selected + per-site grants over tenant-wide scopes.
-
Separate principals per automation domain; no “one ring” app.
-
Key Vault for secrets; managed identity where possible.
-
Conditional Access app exemptions documented; review quarterly.
-
Access reviews: app permissions & owners.
-
Kill switch: disable service principal quickly if needed.
Troubleshooting map (fast)
-
403 Insufficient privileges → wrong scope or missing admin consent.
-
401 Unauthorized → bad/expired secret/cert or token audience mismatch.
-
404 Not found → resource ID/URL wrong or app lacks per-resource grant (e.g., Sites.Selected).
-
429/503 → add backoff/jitter; respect Graph throttling guidance.
Rollout plan (1–2 sprints)
-
Sprint 1: Register app, grant minimal scopes, wire Key Vault, convert one flaky job (SharePoint upload, Teams notify).
-
Sprint 2: Add webhooks, central logging/alerts, rotate secrets, document support playbook; expand to additional workflows.
Anti-patterns to avoid
-
Global admin on a “service user” to “make it work.”
-
Catch-all
*.ReadWrite.Allscopes “just in case.” -
Storing client secrets in code or variables.
-
Skipping admin consent validation—“works on my tenant.”
-
One mega-app for everything (blast radius too big).
Quick-start code sketch (concept)
-
Token: client credentials flow → acquire app token for
https://graph.microsoft.com/.default. -
Call: PUT file to SharePoint drive via site/drive IDs; POST Teams message via channel ID.
-
Retry: on 429/5xx with exponential backoff; log
request-id/client-request-id.
FAQ
Do I need E5?
No. App-only uses Azure AD app registrations + Graph; license needs depend on workloads accessed.
Can app-only read all mail?
Only if you grant that scope. Default is no access; grant least privilege and prefer mailbox-specific patterns.
What about Power Automate?
Use app-only connections (custom connector/HTTP with Azure AD) and bind via environment variables in your ALM pipeline.
Host: Mirko Peters