Building a Microsoft Graph email integration with NestJS sounds simple until OAuth, Azure App Registrations, Outlook accounts, and Exchange Online get involved. Here's the complete journey from authentication failures to a fully working Microsoft 365 email integration.

Building email integrations sounds simple until you actually start connecting Microsoft Graph, Azure App Registrations, Outlook accounts, OAuth flows, and Exchange mailboxes together.
Recently, while building an email platform prototype in NestJS, I spent several hours debugging Microsoft Graph authentication issues. The interesting part was that authentication worked perfectly, user profile endpoints worked, but mailbox APIs kept returning 401 Unauthorized.
After multiple Azure configuration changes, token inspections, and Graph API tests, I finally identified the root cause and got everything working.
In this article, I'll walk through the exact process from start to finish.
The goal was straightforward:
The backend stack was:
Inside Azure Portal:
Microsoft Entra ID
→ App Registrations
I created a new application and configured a redirect URI:
http://localhost:3000/mail/outlook/callback
The application generated:
Next, I created a Client Secret from:
Certificates & Secrets
→ New Client Secret
These values were added to the NestJS environment configuration:
AZURE_CLIENT_ID=...
AZURE_CLIENT_SECRET=...
AZURE_TENANT_ID=...
AZURE_REDIRECT_URI=http://localhost:3000/mail/outlook/callback
Under:
API Permissions
→ Microsoft Graph
→ Delegated Permissions
I added:
openid
profile
offline_access
User.Read
Mail.Read
Mail.ReadWrite
Mail.Send
After adding permissions, I granted admin consent.
This step is critical because OAuth may succeed even when mailbox permissions are missing.
The application exposed two endpoints:
GET /mail/outlook/connect
GET /mail/outlook/callback
The connect endpoint redirected users to Microsoft Login.
After successful authentication, Microsoft redirected back to:
/mail/outlook/callback
where the authorization code was exchanged for:
The first Graph API test was:
GET https://graph.microsoft.com/v1.0/me
This worked immediately.
The API returned:
{
"displayName": "Hardik Kanjariya"
}
At this point I assumed everything was configured correctly.
I was wrong.
The next API call was:
GET https://graph.microsoft.com/v1.0/me/messages
Instead of returning emails, Graph responded with:
401 Unauthorized
Even sending emails failed:
POST https://graph.microsoft.com/v1.0/me/sendMail
The strange part was:
I decoded the access token using JWT inspection tools and checked the scp claim.
The token contained:
Mail.Read
Mail.ReadWrite
Mail.Send
User.Read
profile
openid
This confirmed:
The issue was somewhere else.
Initially I was using a personal Outlook account.
Although authentication succeeded, mailbox-related operations behaved inconsistently.
To eliminate consumer-account edge cases, I created a Microsoft 365 Business Basic subscription.
Microsoft automatically provisioned:
This gave me:
All within the same tenant.
Instead of continuing to debug the original application registration, I created a brand-new App Registration inside the Microsoft 365 tenant.
Configuration:
Single Tenant
Redirect URI:
http://localhost:3000/mail/outlook/callback
Permissions:
User.Read
Mail.Read
Mail.ReadWrite
Mail.Send
offline_access
profile
openid
Then I granted admin consent.
After updating the environment variables:
AZURE_CLIENT_ID=...
AZURE_CLIENT_SECRET=...
AZURE_TENANT_ID=...
I authenticated using:
This time:
GET /me
worked.
GET /me/messages
worked.
POST /me/sendMail
worked.
The integration was finally complete.
Since this was a prototype project, I intentionally avoided unnecessary complexity.
Instead of storing OAuth tokens in a database:
PostgreSQL
Token Tables
User Mapping
I used a simpler approach:
Refresh Token
↓
Store in .env
↓
Generate Access Tokens Automatically
Benefits:
For production applications, token storage should be handled securely in a database or dedicated secrets manager.
The biggest lessons from this implementation were:
/me before testing mailbox endpoints.Microsoft Graph is incredibly powerful once configured correctly. The challenge is usually not the API itself, but understanding how Azure App Registrations, OAuth permissions, Entra ID, and Exchange Online work together.
Once the tenant, mailbox, permissions, and OAuth flow are aligned, reading emails, sending messages, syncing folders, and building custom email clients becomes surprisingly straightforward.
If you're building a NestJS application that integrates with Outlook or Microsoft 365, start with a dedicated business mailbox, keep the initial architecture simple, and verify each step independently before introducing additional complexity.
Get the latest articles, tutorials, and updates delivered straight to your inbox. No spam, unsubscribe at any time.