Bundul
Internal
Browse docs
Waiting for review

how-to

Runbook: Azure App Registration for Microsoft/Outlook Inbox Access

Written by the build · 2 September 2026 · owner @farhan-s · reviewed 2026-09-01

Runbook: Azure App Registration for Microsoft/Outlook Inbox Access

Goal: Register an app in Microsoft Entra ID so Bundul can read a user's Outlook / Microsoft 365 / Outlook.com inbox (delegated, read-only) for bill detection — matching our existing Graph code in src/email-parser/auth/microsoft/.

Time: ~20 minutes. Cost: Free (app registration needs no paid Azure subscription).

Our code already expects these values — the registration just has to produce them:

  • Endpoint: https://login.microsoftonline.com/common/... (multi-tenant + personal accounts)
  • Scopes: offline_access User.Read Mail.Read
  • Env: MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET, MICROSOFT_CALLBACK_URL
  • Prod callback: https://prodapi.bundul.io/api/auth/microsoft/callback

Prerequisites


Step 1 — Create the App Registration

  1. Go to https://entra.microsoft.comIdentityApplicationsApp registrationsNew registration. (Same thing lives in Azure Portal under Microsoft Entra ID → App registrations.)
  2. Name: Bundul Mail Parser — this text appears on the user's consent screen, so keep it clean/branded.
  3. Supported account types: select "Accounts in any organizational directory (Any Microsoft Entra ID tenant – Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)." ⚠️ This exact option is mandatory to support consumer Outlook.com / Hotmail / Live inboxes and work/school accounts. It's what makes the /common/ endpoint in our code valid.
  4. Redirect URI: platform = Web, value = https://prodapi.bundul.io/api/auth/microsoft/callback. (More added in Step 3.)
  5. Click Register.

Step 2 — Copy the Client ID

On the app's Overview page, copy Application (client) ID → this becomes MICROSOFT_CLIENT_ID. (You do not need the Directory/tenant ID because we authenticate against /common.)

Step 3 — Register all redirect URIs

Go to Authentication. Under the Web platform, add every environment's callback:

  • https://prodapi.bundul.io/api/auth/microsoft/callback (prod)
  • https://<staging-host>/api/auth/microsoft/callback (staging, if any)
  • http://localhost:3030/api/auth/microsoft/callback (local dev — http is allowed only for localhost)

Mobile app (MSAL / serverAuthCode flow): if the app performs the OAuth and sends us a serverAuthCode, click + Add a platform → Mobile and desktop applications and register the app's redirect URI (e.g. a custom scheme like msauth.io.bundul://auth or the platform default). The redirect the app uses during /authorize must be listed here or Microsoft rejects the flow. Coordinate the exact value with the mobile team.

Other Authentication settings:

  • Leave Implicit grant (Access tokens / ID tokens) unchecked — we use the auth-code flow.
  • Allow public client flows: No (unless the app uses device-code).
  • Click Save.

Step 4 — Create a Client Secret

  1. Go to Certificates & secrets → Client secrets → New client secret.
  2. Description: bundul-backend; Expiry: pick ≤ 24 months (Azure's max). Note the expiry date.
  3. Click Add, then immediately copy the secret Value (the long string, not the "Secret ID"). It is shown only once.
  4. This becomes MICROSOFT_CLIENT_SECRET.
  5. Set a calendar reminder to rotate it ~2 weeks before expiry — when it lapses, every token refresh fails with a 500 and all Microsoft inboxes stop syncing. (Prod-grade alternative: upload a certificate instead of a secret.)

Step 5 — Add Graph API Permissions (Delegated)

  1. Go to API permissions → Add a permission → Microsoft Graph → Delegated permissions.
  2. Add these:
    • User.Read (usually present by default)
    • Mail.Read
    • offline_access
    • (optional OIDC: openid, email, profile )
  3. These are all user-consentable, so you do not need to click "Grant admin consent" for consumer or typical work accounts.
    • Exception: some corporate tenants enforce an admin-consent policy — in that case their IT admin approves it on first sign-in. Nothing you configure changes that.
  4. Mail.Read is read-only (parity with our gmail.readonly). If we ever add sending, that's a separate Mail.Send permission + re-consent.

Step 6 — (Recommended) Branding & Publisher Verification

Go to Branding & properties:

  • Set logo, Publisher display name, Terms of service URL, Privacy statement URL.
  • Complete Publisher verification to remove the "unverified app" warning on the consent screen (needs a verified Microsoft Partner/MPN account). Strongly recommended before consumer launch — it's the difference between a trusted consent screen and a scary one.

Step 7 — Wire the values into our config

Azure output Env var Notes
Application (client) ID MICROSOFT_CLIENT_ID Overview page
Client secret Value MICROSOFT_CLIENT_SECRET Store in secrets manager, not committed .env
Chosen callback per env MICROSOFT_CALLBACK_URL e.g. https://prodapi.bundul.io/api/auth/microsoft/callback

Document all three in .env.example. Our code (microsoft-auth.service.ts, microsoft.strategy.ts) reads exactly these keys and falls back to the prod callback if MICROSOFT_CALLBACK_URL is unset.

Step 8 — Smoke-test the consent + token flow

  1. Paste this authorize URL in a browser (replace {CLIENT_ID}), sign in, and approve:
    https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id={CLIENT_ID}&response_type=code&redirect_uri=https%3A%2F%2Fprodapi.bundul.io%2Fapi%2Fauth%2Fmicrosoft%2Fcallback&response_mode=query&scope=offline_access%20User.Read%20Mail.Read&prompt=consent
    
  2. You should land on the redirect URI with ?code=.... That confirms client ID, redirect URI, account types, and scopes are all correct.
  3. Then run the real flow end-to-end via the app's connectMicrosoftForMailParser mutation and confirm a token row lands in UserEmailMailAccess with provider: 'microsoft'.
  4. Test twice: once with a consumer outlook.com account, once with a Microsoft 365 work account — consent and refresh-token behavior differ between them.

Gotchas checklist

  • Account type is multitenant + personal (not single-tenant) — or consumer inboxes fail.
  • Every redirect URI the client uses (web + mobile + local) is registered, exact-match including trailing path.
  • Secret Value captured (not Secret ID); expiry reminder set.
  • Delegated Mail.Read + offline_access present (offline_access is what gives us the refresh token).
  • Secret lives in secrets manager, not the repo.
  • Personal-account refresh tokens can expire after ~90 days of inactivity → app needs a "reconnect" prompt when refresh fails (backend change, tracked separately).
  • Publisher verification done before consumer launch to avoid the "unverified" consent warning.