Skip to main content

Azure Email Setup Guide

Application: MOC & PCR Management System
Email Provider: Microsoft Graph API (Azure)
Audience: IT Administrator / System Owner


Overview​

The MOC-PCR system sends transactional emails (notifications, reminders, approvals) through the application backend.

Emails are delivered using the Microsoft Graph API via a registered Azure application with Mail.Send permission. No external email gateway, third-party SMTP relay, or SES account is required — only your existing Microsoft 365 / Exchange Online environment.

The email system is also deeply integrated with the in-app notification layer. Even if an email delivery attempt fails, the in-app notification is still saved to the database so users can always see the event inside the application.


Architecture Flow​

Application Backend (sendEmail)
│
▼
Acquire OAuth2 token from Microsoft Entra ID
(cached in memory — refreshed automatically before expiry)
│
▼
POST /v1.0/users/{sender}/sendMail
(Microsoft Graph API)
│
▼
Email delivered
│
▼
In-app notification saved to database
(always fires — even if email delivery fails)

Tokens are cached in memory and automatically refreshed two minutes before expiry, so no repeated round-trips to Microsoft are made per email.


Prerequisites​

RequirementDetails
Azure SubscriptionActive subscription with an admin account
Microsoft Entra IDTenant already provisioned (same tenant used for SSO is fine)
Licensed Exchange mailboxThe sender mailbox must have an Exchange Online / M365 licence
Application serverMust be able to reach login.microsoftonline.com and graph.microsoft.com

Important: The sender account must have an active Exchange Online mailbox. A shared mailbox or a licensed M365 user both work. A plain Entra ID identity with no Exchange licence cannot send mail via Graph.


Part 1 — Azure Portal Configuration​

Step 1 — Register (or Reuse) the Application​

If you already completed the SSO setup you already have an app registration. You can reuse the same registration — just add the permissions below to it. Skip to Step 2 if so.

Otherwise:

  1. Sign in to the Azure Portal.
  2. Search for and open Microsoft Entra ID.
  3. Click App registrations → New registration.
  4. Fill in:
    • Name: MOC-PCR System (or your existing name)
    • Supported account types: Accounts in this organizational directory only
    • Redirect URI: leave blank (not needed for this flow)
  5. Click Register.

Copy and save the Application (client) ID and Directory (tenant) ID from the overview page.


Step 2 — Add Mail.Send Permission​

  1. Inside the app registration, go to API permissions → Add a permission.
  2. Select Microsoft Graph → Application permissions.
  3. Search for and select Mail.Send.
  4. Click Add permissions.
  5. Click Grant admin consent for [Your Organization] → Yes.

Mail.Send must show a green ✓ Granted status before continuing.

Application permissions vs Delegated permissions:
This flow uses application permissions (client credentials grant, no user interaction). Do not add Mail.Send as a Delegated permission — that requires a signed-in user and will not work from a server process.


Step 3 — Create (or Reuse) a Client Secret​

If you already have a client secret from the SSO setup, you can reuse it.

Otherwise:

  1. Go to Certificates & secrets → Client secrets → New client secret.
  2. Set a description (e.g., MOC-PCR Mail Secret) and an expiry (12 or 24 months recommended).
  3. Click Add.
  4. Immediately copy the Value column — it is shown only once.

⚠️ Azure shows two columns after creating a secret:

  • Secret ID — a GUID for Azure Portal management only. Do not use this.
  • Value — the actual secret (e.g., mXK8Q~...). This goes in your environment variables.

Step 4 — Identify the Sender Mailbox​

The Graph API sends mail on behalf of a specific user (or shared mailbox). This account must:

  • Have an active Exchange Online / Microsoft 365 licence, or be a shared mailbox.
  • Be in the same tenant as the registered application.

Note the full email address of this account (e.g., no-reply@yourcompany.com). This becomes the GRAPH_MAIL_SENDER environment variable.


Part 2 — Application Environment Variables​

Set the following variables on the server where the application is deployed. These must never be committed to source control.

# ── Email provider (must be set to "graph") ─────────────────────────────────
EMAIL_PROVIDER=graph

# ── Microsoft Graph Mail ────────────────────────────────────────────────────
AZURE_TENANT_ID=<Directory (tenant) ID>
AZURE_CLIENT_ID=<Application (client) ID>
AZURE_CLIENT_SECRET=<Secret VALUE — NOT the Secret ID>
GRAPH_MAIL_SENDER=<sender email address, e.g. no-reply@yourcompany.com>

AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET are the same values already configured for SSO. You do not need a separate app registration.

Verification Checklist​

VariableExpected FormatWhere to Find
EMAIL_PROVIDERMust be the literal string graphSet manually
AZURE_TENANT_IDUUID (e.g. 10091248-3181-...)App registration → Directory (tenant) ID
AZURE_CLIENT_IDUUID (e.g. 3dd121a7-9c06-...)App registration → Application (client) ID
AZURE_CLIENT_SECRETAlphanumeric string with special chars (e.g. mXK8Q~8vJ...)Certificates & secrets → Value column only
GRAPH_MAIL_SENDERFull email address (e.g. no-reply@yourcompany.com)The licensed mailbox chosen in Step 4

Part 3 — Token Caching​

The application caches the OAuth2 access token in memory:

  • Tokens are valid for 60 minutes (standard Microsoft token lifetime).
  • The application refreshes the token 2 minutes before expiry to ensure no expired tokens are ever used.
  • On a cold start (fresh server boot) the first email triggers a token fetch. All subsequent emails within 58 minutes reuse the cached token with no extra network call.
  • The cache lives in the Node.js server process. In a multi-process environment (e.g., PM2 cluster mode or multiple Docker replicas), each process maintains its own token — this is safe and correct.

Part 4 — Secret Rotation​

Client secrets expire. Before the secret reaches its expiry date:

  1. Go to Azure Portal → App registrations → your app → Certificates & secrets.
  2. Create a new client secret.
  3. Update AZURE_CLIENT_SECRET in your environment variables with the new Value.
  4. Restart the application server.
  5. Confirm emails are being sent, then delete the old secret from Azure Portal.

Recommended: Set a calendar reminder 30 days before the secret's expiry date.


Part 5 — Email Send Flow (Technical Reference)​

sendEmail(payload)
│
▼
getGraphToken()
│
├─ cache hit? ──▶ return cached token
│
└─ cache miss? ──▶ POST /oauth2/v2.0/token (client_credentials)
│
▼
Cache token (expires_in - 120s)
│
▼
POST /v1.0/users/{sender}/sendMail
Authorization: Bearer {token}
│
├─ success ──▶ { success: true }
│
└─ failure ──▶ error logged — execution continues below
│
▼
sendNotification() — ALWAYS fires (success or failure)
Persists in-app notification to database

Troubleshooting​

Error messageLikely causeResolution
Graph token request failed: ...Wrong AZURE_TENANT_ID, AZURE_CLIENT_ID, or AZURE_CLIENT_SECRETVerify all three values; ensure the secret has not expired
GRAPH_MAIL_SENDER env variable is missingGRAPH_MAIL_SENDER not setAdd the full sender email address to environment variables
Graph email failed: 403 ...Mail.Send permission not granted, or admin consent not givenGo to API permissions → Grant admin consent
Graph email failed: 404 ...Sender mailbox does not exist or has no Exchange licenceConfirm the sender account exists and has an active M365 / Exchange Online licence
Graph email failed: 401 ...Token invalid or expired mid-requestShould auto-recover on next call; check that system clock is accurate

Next Steps​

With Microsoft Graph email configured and all environment variables noted, you now have everything needed for deployment. Proceed to:

Docker Deployment →


Last updated: March 2026