Skip to content

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


AppApp IDPermissionTypeConsent
OpenInsure Auth790becb2-6ec9-48bd-9a16-3b60861511d7User.Read, openid, profileDelegatedUser-level
MGA Mailbox Ingest6d155a54-3bce-4bde-9302-0c7276c7bea7Mail.ReadWriteApplication✅ 2026-03-08
Notifications Botac81bbbd-53fe-4b8a-8755-dafded231e19Mail.SendApplication✅ 2026-03-07

The Auth app registration powers Microsoft SSO across all portals. When a user clicks “Sign in with Microsoft”:

  1. Browser redirects to https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize
  2. User authenticates with their @mhcmga.com or @openinsure.dev account
  3. Microsoft returns an authorization code to https://auth.openinsure.dev/callback/microsoft
  4. Auth worker (oi-sys-auth) exchanges the code for tokens and issues a portal JWT
  5. 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/microsoft
  • http://localhost:3000/callback/microsoft (dev)

OpenInsure sends transactional email through the Microsoft Graph sendMail endpoint using OAuth2 client credentials (app-only — no user sign-in required).

The queue consumer in packages/notify/src/index.ts checks EMAIL_PROVIDER:

ValueProvider
graphMicrosoft Graph (/v1.0/users/{from}/sendMail)
resendResend API
smtpSMTP relay

Set EMAIL_PROVIDER=graph in .dev.vars for local dev or in wrangler.toml [vars] for production.

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.

Terminal window
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.


oi-sys-api polls Underwriting@mhcmga.com every 15 minutes to capture inbound submissions and correspondence.

  1. 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-assets 3. Route — Each message is handed to the EmailIntakeAgent Durable Object 4. Mark — Messages are marked read and moved to a processed folder

The cron silently no-ops if GRAPH_SHARED_MAILBOX is not set. This prevents errors in environments where the mailbox integration isn’t configured.

Terminal window
wrangler secret put GRAPH_SHARED_MAILBOX # Underwriting@mhcmga.com
# Also uses AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET (same as email)
[triggers]
crons = ["0 6 * * *", "*/15 * * * *"]
# "*/15 * * * *" — mailbox ingest
# "0 6 * * *" — daily portfolio sweep
FileRole
apps/api/src/cron/mailbox-ingest.tsCron handler
packages/notify/src/graph-mailbox.tscreateGraphMailboxReader()
packages/agents/src/agents/EmailIntakeAgent.tsDO that processes each message

An Exchange Online ApplicationAccessPolicy restricts the app’s Mail.ReadWrite permission to Underwriting@mhcmga.com only, preventing access to other tenant mailboxes.

To verify:

Terminal window
Test-ApplicationAccessPolicy -AppId 6d155a54-3bce-4bde-9302-0c7276c7bea7 `
-Identity Underwriting@mhcmga.com
# Expected: AccessCheckResult = Granted

The Notifications Bot sends messages to Microsoft Teams channels via the Bot Framework.

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:

Terminal window
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.

Terminal window
POST /v1/notifications/dispatch
{
"channels": ["teams"],
"teams": { "to": "underwriting@mhcmga.com" },
"template": "submission_referred",
"templateData": { "submissionId": "...", "risk": "General Liability" }
}
Terminal window
# 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"

Client secrets expire. The current oi-api-worker-2026 secret expires 2028-03-08.

  1. 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 (or TEAMS_BOT_APP_PASSWORD) 3. Record the new expiry in docs/SECRETS_INVENTORY.md 4. Revoke the old secret in Azure Portal 5. Verify the next cron run in Cloudflare Dashboard logs (no auth errors)

  • docs/SECRETS_INVENTORY.md — Full secrets inventory with expiry dates and rotation history
  • docs/archive/TEAMS_NOTIFICATIONS_SETUP.md — Teams Bot initial provisioning walkthrough
  • docs/archive/AZURE_AD_ADMIN_CONSENT_GUIDE.md — Step-by-step admin consent procedure
  • infra/azure-auth/main.tf — Terraform for the Auth app registration