Microsoft 365 Integration
OpenInsure connects to Microsoft 365 through three Azure AD app registrations in the PCC MHC tenant. Together they handle SSO authentication, automated mailbox ingestion, and Teams channel notifications.
Tenant
ebd58a52-c818-4230-b150-348ae1e17975
palmettoconsulting.us / pcc-mhcis-001
Subscription
pcc-mhcis-001
b18f2221-95c0-4eea-936c-0c058e3a8ba1
App Registrations
| App | App ID | Permission | Type | Consent |
|---|---|---|---|---|
| OpenInsure Auth | 790becb2-6ec9-48bd-9a16-3b60861511d7 | User.Read, openid, profile | Delegated | User-level |
| MGA Mailbox Ingest | 6d155a54-3bce-4bde-9302-0c7276c7bea7 | Mail.ReadWrite | Application | ✅ 2026-03-08 |
| Notifications Bot | ac81bbbd-53fe-4b8a-8755-dafded231e19 | Mail.Send | Application | ✅ 2026-03-07 |
The Notifications Bot currently has Mail.Send only. Teams channel messaging requires
ChatMessage.Send (permission ID 001f47b3-a01f-4dcd-9bf7-6baf0ac00b88). Teams notifications
will fail silently until this permission is granted and admin-consented. See Fix Teams
Messaging below.
Authentication (OpenInsure Auth)
The Auth app registration powers Microsoft SSO across all portals. When a user clicks "Sign in with Microsoft":
- Browser redirects to
https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize - User authenticates with their
@mhcmga.comor@openinsure.devaccount - Microsoft returns an authorization code to
https://auth.openinsure.dev/callback/microsoft - Auth worker (
oi-sys-auth) exchanges the code for tokens and issues a portal JWT - Portal cookie is set; user lands on their dashboard
IaC: infra/azure-auth/main.tf provisions this app registration via Terraform.
Redirect URIs configured:
https://auth.openinsure.dev/callback/microsofthttp://localhost:3000/callback/microsoft(dev)
Email via Microsoft Graph
OpenInsure sends transactional email through the Microsoft Graph sendMail endpoint using OAuth2 client credentials (app-only — no user sign-in required).
How the provider is selected
The queue consumer in packages/notify/src/index.ts checks EMAIL_PROVIDER:
| Value | Provider |
|---|---|
graph | Microsoft Graph (/v1.0/users/{from}/sendMail) |
resend | Resend API |
smtp | SMTP relay |
Set EMAIL_PROVIDER=graph in .dev.vars for local dev or in wrangler.toml [vars] for production.
Token flow
The Graph provider fetches an OAuth2 client credentials token from:
POST https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token
scope = https://graph.microsoft.com/.default
grant_type = client_credentials
Tokens are cached in-memory and refreshed 60 seconds before expiry. No external token store is required.
Required secrets
wrangler secret put AZURE_TENANT_ID # ebd58a52-c818-4230-b150-348ae1e17975
wrangler secret put AZURE_CLIENT_ID # 6d155a54-3bce-4bde-9302-0c7276c7bea7
wrangler secret put AZURE_CLIENT_SECRET # from Azure Portal → app → Certificates & secrets
wrangler secret put GRAPH_MAIL_FROM # jd@openinsure.dev (licensed O365 mailbox)
The sending mailbox (GRAPH_MAIL_FROM) must be a licensed Exchange Online mailbox — shared mailboxes without a license cannot send via Graph.
Mailbox Ingest Cron
oi-sys-api polls Underwriting@mhcmga.com every 15 minutes to capture inbound submissions and correspondence.
What happens each run
- Read — Graph API fetches unread messages from the shared mailbox inbox 2. Download —
Attachments (ACORD forms, loss runs, etc.) are written to R2 bucket
oi-assets3. Route — Each message is handed to theEmailIntakeAgentDurable Object 4. Mark — Messages are marked read and moved to a processed folder
Guard condition
The cron silently no-ops if GRAPH_SHARED_MAILBOX is not set. This prevents errors in environments where the mailbox integration isn't configured.
Required secrets
wrangler secret put GRAPH_SHARED_MAILBOX # Underwriting@mhcmga.com
# Also uses AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET (same as email)
Cron declaration (wrangler.toml)
[triggers]
crons = ["0 6 * * *", "*/15 * * * *"]
# "*/15 * * * *" — mailbox ingest
# "0 6 * * *" — daily portfolio sweep
Code paths
| File | Role |
|---|---|
apps/api/src/cron/mailbox-ingest.ts | Cron handler |
packages/notify/src/graph-mailbox.ts | createGraphMailboxReader() |
packages/agents/src/agents/EmailIntakeAgent.ts | DO that processes each message |
Security hardening
An Exchange Online ApplicationAccessPolicy restricts the app's Mail.ReadWrite permission to Underwriting@mhcmga.com only, preventing access to other tenant mailboxes.
To verify:
Test-ApplicationAccessPolicy -AppId 6d155a54-3bce-4bde-9302-0c7276c7bea7 `
-Identity Underwriting@mhcmga.com
# Expected: AccessCheckResult = Granted
Teams Bot Notifications
The Notifications Bot sends messages to Microsoft Teams channels via the Bot Framework.
Configuration
The app ID and tenant are non-secret and committed to wrangler.toml:
[vars]
TEAMS_BOT_APP_ID = "ac81bbbd-53fe-4b8a-8755-dafded231e19"
TEAMS_BOT_TENANT_ID = "ebd58a52-c818-4230-b150-348ae1e17975"
The client secret is injected at deploy time:
wrangler secret put TEAMS_BOT_APP_PASSWORD
Teams dispatch is automatically skipped if any of the three variables is absent — the route returns normally with teams: null in the dispatch result.
Dispatch via API
POST /v1/notifications/dispatch
{
"channels": ["teams"],
"teams": { "to": "underwriting@mhcmga.com" },
"template": "submission_referred",
"templateData": { "submissionId": "...", "risk": "General Liability" }
}
Fix Teams Messaging
Teams messaging requires ChatMessage.Send — not currently granted. Until fixed, Teams
notifications fail silently and only Mail.Send (email) works.
# 1. Add the permission
az ad app permission add \
--id ac81bbbd-53fe-4b8a-8755-dafded231e19 \
--api 00000003-0000-0000-c000-000000000000 \
--api-permissions 001f47b3-a01f-4dcd-9bf7-6baf0ac00b88=Role
# 2. Grant admin consent
az ad app permission admin-consent \
--id ac81bbbd-53fe-4b8a-8755-dafded231e19
# 3. Verify
az ad app show \
--id ac81bbbd-53fe-4b8a-8755-dafded231e19 \
--query "requiredResourceAccess[].resourceAccess[].id"
Credential Rotation
Client secrets expire. The current oi-api-worker-2026 secret expires 2028-03-08.
- Create a new secret in Azure Portal → App Registration → Certificates & secrets → New
client secret 2. Update the Worker secret:
wrangler secret put AZURE_CLIENT_SECRET(orTEAMS_BOT_APP_PASSWORD) 3. Record the new expiry indocs/SECRETS_INVENTORY.md4. Revoke the old secret in Azure Portal 5. Verify the next cron run in Cloudflare Dashboard logs (no auth errors)
Set a calendar reminder 30 days before expiry. A lapsed secret silently breaks email delivery and mailbox ingest — there are no automatic Cloudflare alerts for expired Graph tokens.
Reference
docs/SECRETS_INVENTORY.md— Full secrets inventory with expiry dates and rotation historydocs/archive/TEAMS_NOTIFICATIONS_SETUP.md— Teams Bot initial provisioning walkthroughdocs/archive/AZURE_AD_ADMIN_CONSENT_GUIDE.md— Step-by-step admin consent procedureinfra/azure-auth/main.tf— Terraform for the Auth app registration