Microsoft Graph OAuth: Authenticate Outlook and Microsoft 365 Mailboxes (2026 Guide)

Microsoft Graph OAuth 2026

Microsoft Graph OAuth: Authenticate Outlook and Microsoft 365 Mailboxes

A complete 2026 guide to Microsoft Graph OAuth for SaaS developers. Covers Microsoft Entra app registration, authority endpoints, Mail scopes, delegated vs application permissions, admin consent, auth code + PKCE, refresh token rotation, AADSTS error codes, and how Unipile eliminates 5 weeks of OAuth plumbing in 5 minutes.

link_outlook_account.py
import requests # Step 1: Generate a hosted auth link response = requests.post( "https://apiXXX.unipile.com:XXX /api/v1/hosted/accounts/link", headers={"X-API-KEY": api_key}, json={ "type": "create", "providers": ["MICROSOFT"], "api_url": your_dsn, "expiresOn": "2026-12-31T23:59:59Z" } ) # Step 2: Redirect your user to the URL auth_url = response.json()["url"] # Unipile handles the full OAuth flow # incl. Entra, scopes, tokens, refresh
Microsoft OAuth handled. Mailbox ready.
2026 Context

Why Microsoft Graph OAuth is Non-Negotiable in 2026

If your SaaS application reads, sends, or syncs Outlook or Microsoft 365 email, Microsoft Graph OAuth is no longer optional. Three major deprecations have made Basic Auth, legacy protocols, and app passwords obsolete. OAuth 2.0 via the Microsoft Graph API is the only supported path.

Basic Auth - Fully Deprecated
Microsoft disabled Basic Authentication for Exchange Online in October 2022. This affects IMAP, POP3, SMTP, EWS, MAPI, OAB, Outlook Anywhere, and RPC over HTTP. No exceptions, no grace period extensions remaining.
Disabled Oct 2022
EWS - Sunset Timeline
Exchange Web Services (EWS) is in maintenance mode. Microsoft has announced no new features and recommends migrating to Microsoft Graph. While not fully shut down yet, building new integrations on EWS in 2026 is a technical debt decision you will regret.
Maintenance Mode
OAuth 2.0 - The Required Standard
Microsoft Graph OAuth via Microsoft Entra ID (formerly Azure AD) is the only officially supported authentication method for production SaaS applications accessing Outlook and Microsoft 365 mailboxes in 2026. This guide covers the complete implementation.
Required Standard
What Microsoft Graph OAuth unlocks
Read, send, and sync Outlook mailboxes on behalf of authenticated users
Access Microsoft 365 and personal Outlook.com mailboxes with a single app registration
Long-lived refresh tokens with automatic rotation (no re-auth for active users)
Granular scope control: request only the permissions your app genuinely needs
Multi-tenant support: one app registration serves all customer tenants
Compliance with enterprise Conditional Access and MFA policies
OAuth Flows

The 3 OAuth Flows Microsoft Supports (and Which One SaaS Needs)

Microsoft's identity platform supports several OAuth 2.0 grant types. Choosing the wrong one is a common source of wasted engineering time. Here is the breakdown for SaaS applications that access email on behalf of their users.

Client Credentials
OAuth 2.0 Section 4.4 (app-only)
No user interaction. Your app authenticates as itself using a client ID and secret (or certificate). Requires Application permissions (not Delegated), meaning a tenant admin must grant consent to access all mailboxes in the organization. No user consent screen.
Only for internal tooling with admin-granted access
Device Code Flow
OAuth 2.0 Device Authorization Grant
Designed for devices without a browser (CLIs, IoT, smart TVs). The user visits a URL on another device to authenticate. Not relevant for SaaS web applications where a redirect is possible.
Not applicable to standard SaaS web apps
Unipile - Microsoft Entra App Registration
Step-by-Step Setup

Microsoft Entra App Registration: 7 Steps

Before your app can request OAuth tokens, you need a registered application in Microsoft Entra ID (formerly Azure Active Directory). Here are the exact steps, including which field values matter and which are cosmetic.

1
Create an App Registration in the Azure Portal
Navigate to portal.azure.com, go to Microsoft Entra ID (search in the top bar), then App registrations, then click + New registration. See the full Microsoft OAuth documentation for additional context.
Name
Your app name. This is what users see on the Microsoft consent screen. Use your product name, not a technical identifier.
Supported account types
For a multi-tenant SaaS serving both business and personal users: select Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant) and personal Microsoft accounts. This corresponds to the /common authority endpoint.
Tip: If you only serve Microsoft 365 business accounts (not personal Outlook.com), select "Multitenant" only. This uses the /organizations authority and reduces your consent surface area. More on authority endpoints in section 4.
2
Configure Redirect URIs
After registration, go to Authentication in the left panel. Add a platform: select Web. Add your redirect URI(s), the URL Microsoft will redirect the user back to with the authorization code.
Platform type
Web for server-side apps. Use Single-page application (SPA) for client-side flows (auto-enables PKCE).
Redirect URI
Must be HTTPS in production. Example: https://app.yourproduct.com/auth/microsoft/callback. Exact match required, any deviation causes AADSTS50011.
Logout URL (optional)
Microsoft will call this URL when the user signs out from any app in their tenant. Optional for email-only integrations.
Common mistake: localhost URIs are allowed during development (http://localhost:3000/callback) but production URIs must use HTTPS. Register both separately.
3
Add Microsoft Graph API Permissions
Go to API permissions. Click + Add a permission, select Microsoft Graph, then Delegated permissions. Add the scopes your app needs.
Minimum for read
Mail.Read, offline_access, openid, profile
For read + send
Mail.ReadWrite, Mail.Send, offline_access, openid, profile
offline_access
Critical. Without this scope, Microsoft does not issue a refresh token. Your users will need to re-authenticate every 60-90 minutes.
Note: Adding permissions here does not grant them, it declares your intent. The user consents when they complete the OAuth flow. Admin consent is required for some higher-privilege scopes (see section 7).
4
Generate a Client Secret
Go to Certificates and secrets. Click + New client secret. Set a description and expiry.
Expiry recommendation
730 days (24 months) is the maximum. Set a calendar reminder 60 days before expiry, rotating an expired secret causes immediate auth failures across all users.
Copy the value immediately
The secret value is shown only once. Store it in your secrets manager (e.g., AWS Secrets Manager, HashiCorp Vault, Azure Key Vault). It will never be shown again after you leave this page.
Recommended alternative: For production, use a certificate instead of a client secret. Certificates are more secure (asymmetric key), never expire by accident, and are preferred by Microsoft for enterprise-grade apps.
5
Copy Your Client ID and Tenant ID
From the Overview page of your app registration, copy two values you will need in every OAuth request:
Application (client) ID
A UUID that uniquely identifies your app registration. Used as the client_id parameter in all auth requests.
Directory (tenant) ID
Your organization's tenant ID. Used in single-tenant authority URLs. For multi-tenant apps, you use /common instead, but keep this value for admin consent URLs.
6
Enable ID Tokens (Optional but Recommended)
Back in Authentication, under Implicit grant and hybrid flows, you can enable ID tokens. This lets you receive a JWT with user identity claims (name, email, tenant ID) alongside the access token, useful for onboarding flows where you want to pre-fill user profile data.
2026 guidance: For pure auth-code + PKCE flows, ID tokens are returned via the token endpoint, not implicit grant. You do not need to enable implicit grant. Only enable it if you have a legacy SPA that cannot use PKCE.
7
Optional: Verify Your Publisher Domain
In Branding and properties, set your publisher domain to your product's domain. This replaces the "unverified" label on the consent screen with your domain name. For multi-tenant apps targeting enterprise customers, completing the Microsoft Partner Network verification and becoming a "verified publisher" removes the prominent "This app is not commonly used" warning.
Publisher domain
A domain you own and have verified via TXT record or via the App Service domain validation flow.
Verified publisher
Requires Microsoft Partner Network (MPN) ID linked to a verified business identity. Takes 1-5 business days. Significantly improves enterprise customer conversion on consent screens.
Authority Endpoints

Choosing the Right Microsoft Authority Endpoint

The authority URL you use in your OAuth requests determines which types of Microsoft accounts can authenticate and what tokens you receive. Getting this wrong causes silent failures where some users cannot authenticate at all.

Authority URL Accepts Use Case Caveats
/commonMost SaaS Both Microsoft Entra (work/school) and personal Microsoft accounts (Outlook.com, Hotmail, Live) Multi-tenant SaaS serving all Microsoft users. One endpoint handles your entire user base. Tokens are issued by each user's home tenant, not yours. Token validation must use tenant-specific issuer or accept multiple issuers. Cannot enforce Conditional Access policies.
/organizations Microsoft Entra ID accounts only (work/school). No personal Microsoft accounts. B2B SaaS targeting only enterprise customers, never consumer Outlook.com users. Users with personal Microsoft accounts will receive an error. Simpler token validation (single issuer pattern acceptable).
/consumers Personal Microsoft accounts only (Outlook.com, Hotmail, Live). Consumer apps targeting personal inboxes. Rare for B2B SaaS. Enterprise Microsoft 365 accounts are rejected. Not useful for SaaS serving business users.
/{tenant-id} Accounts in a specific Microsoft Entra tenant only. Single-tenant internal tooling (your own company's app). Admin consent flows targeting a specific tenant. Also used in the admin consent redirect URL pattern. Every other tenant's users are rejected. Only appropriate for internal apps or when deliberately locking to one customer's tenant.
/common Most SaaS
Accepts Both Microsoft Entra (work/school) and personal Microsoft accounts (Outlook.com, Hotmail, Live)
Use case Multi-tenant SaaS serving all Microsoft users. One endpoint handles your entire user base.
Caveats Tokens are issued by each user's home tenant, not yours. Token validation must use tenant-specific issuer or accept multiple issuers. Cannot enforce Conditional Access policies.
/organizations
Accepts Microsoft Entra ID accounts only (work/school). No personal Microsoft accounts.
Use case B2B SaaS targeting only enterprise customers, never consumer Outlook.com users.
Caveats Users with personal Microsoft accounts will receive an error. Simpler token validation (single issuer pattern acceptable).
/consumers
Accepts Personal Microsoft accounts only (Outlook.com, Hotmail, Live).
Use case Consumer apps targeting personal inboxes. Rare for B2B SaaS.
Caveats Enterprise Microsoft 365 accounts are rejected. Not useful for SaaS serving business users.
/{tenant-id}
Accepts Accounts in a specific Microsoft Entra tenant only.
Use case Single-tenant internal tooling (your own company's app). Admin consent flows targeting a specific tenant. Also used in the admin consent redirect URL pattern.
Caveats Every other tenant's users are rejected. Only appropriate for internal apps or when deliberately locking to one customer's tenant.
Token validation note for /common: When using the /common endpoint, the iss (issuer) claim in the JWT will be https://login.microsoftonline.com/{tenantId}/v2.0 where {tenantId} varies per user. Configure your JWT validation library to accept any issuer matching https://login.microsoftonline.com/{tenantId}/v2.0 rather than a fixed issuer string.
Mail Scopes

Microsoft Graph Mail Scopes: Granular Breakdown

Microsoft Graph uses permission scopes to control what your application can do. Requesting too many scopes increases friction on the consent screen and reduces conversion. Requesting too few causes runtime errors. Here is every Mail scope you need to know.

Scope Type What it enables Admin consent?
Mail.Read Delegated Read all messages in the authenticated user's mailbox. Includes headers, body, attachments. Read-only - cannot modify or send. User
Mail.ReadBasic Delegated Read limited message properties: subject, sender, recipients, date. Cannot read message body or attachments. Useful for lightweight inbox listing without full content access. User
Mail.ReadWrite Delegated Read and modify all messages. Includes creating, updating, deleting messages and folders. Superset of Mail.Read - do not request both. User
Mail.Send Delegated Send emails as the authenticated user. Required even if you also have Mail.ReadWrite - sending is a separate permission in Microsoft Graph. User
Mail.Read.Shared Delegated Read mail in shared mailboxes or other users' mailboxes that the authenticated user has been granted access to. Not for reading the user's own mailbox. User
Mail.ReadWrite.Shared Delegated Read and modify mail in shared mailboxes the user has access to. User
Mail.Send.Shared Delegated Send email from shared mailboxes or "send on behalf of" another user (if that user has granted access). User
offline_access Delegated Instructs Microsoft to issue a refresh token. Without this, you only receive a short-lived access token with no way to renew it. Always required for SaaS applications. User
openid Delegated Returns an ID token with basic user identity. Required if you want to know who authenticated without making a separate /me API call. User
profile Delegated Adds name and preferred_username claims to the ID token. Usually included with openid. User
Mail.Read (App) Application Read all mail in all mailboxes in the tenant without user interaction. Used by daemon services. Requires tenant admin consent. Admin required
Mail.ReadWrite (App) Application Read and write all mail in all tenant mailboxes. Very broad permission. Only for trusted internal tooling with explicit tenant admin approval. Admin required
Mail.Read Delegated
Read all messages in the authenticated user's mailbox. Includes headers, body, attachments. Read-only - cannot modify or send.
User consent
Mail.ReadBasic Delegated
Read limited message properties: subject, sender, recipients, date. Cannot read message body or attachments. Useful for lightweight inbox listing without full content access.
User consent
Mail.ReadWrite Delegated
Read and modify all messages. Includes creating, updating, deleting messages and folders. Superset of Mail.Read - do not request both.
User consent
Mail.Send Delegated
Send emails as the authenticated user. Required even if you also have Mail.ReadWrite - sending is a separate permission in Microsoft Graph.
User consent
Mail.Read.Shared Delegated
Read mail in shared mailboxes or other users' mailboxes that the authenticated user has been granted access to. Not for reading the user's own mailbox.
User consent
Mail.ReadWrite.Shared Delegated
Read and modify mail in shared mailboxes the user has access to.
User consent
Mail.Send.Shared Delegated
Send email from shared mailboxes or "send on behalf of" another user (if that user has granted access).
User consent
offline_access Delegated
Instructs Microsoft to issue a refresh token. Without this, you only receive a short-lived access token with no way to renew it. Always required for SaaS applications.
User consent
openid Delegated
Returns an ID token with basic user identity. Required if you want to know who authenticated without making a separate /me API call.
User consent
profile Delegated
Adds name and preferred_username claims to the ID token. Usually included with openid.
User consent
Mail.Read (App) Application
Read all mail in all mailboxes in the tenant without user interaction. Used by daemon services. Requires tenant admin consent.
Admin required
Mail.ReadWrite (App) Application
Read and write all mail in all tenant mailboxes. Very broad permission. Only for trusted internal tooling with explicit tenant admin approval.
Admin required
Minimum scope set: inbox reader
scope=Mail.Read%20offline_access%20openid%20profile
Read messages, refresh tokens, user identity. No write or send capability.
Standard scope set: full email integration
scope=Mail.ReadWrite%20Mail.Send%20offline_access%20openid%20profile
Read, write, send. The most common set for CRM and sales tool integrations.
See the full Email API guide
Permissions Model

Delegated vs Application Permissions: When Each Applies

Microsoft Graph uses two fundamentally different permission models. Most SaaS developers default to the wrong one, which leads to unnecessary admin consent requirements and a broken user experience. Here is exactly when to use each.

Delegated Permissions
Act on behalf of the signed-in user
The app accesses Microsoft Graph using the identity of the authenticated user. The app can only do what the user themselves could do. If the user cannot read a folder, neither can your app.
User consents during OAuth flow - no admin required for standard scopes
Access is scoped to each individual user's mailbox
Users can revoke access at any time from their Microsoft account settings
Respects user-level permissions, role assignments, and mailbox access policies
Use this for SaaS applications where each user authenticates individually
Application Permissions
Act as the application itself
The app accesses Microsoft Graph without any user being present. Used for background services, daemons, and automated workflows. The app authenticates using its own credentials (client credentials flow).
Requires tenant admin consent - a significant barrier for external users
Access is tenant-wide - can read ALL mailboxes once admin consents
No interactive user sign-in required - works for headless automation
Suitable for internal IT tools where your org's admin controls deployment
Only for internal tooling where your org's admin grants full tenant access
The SaaS Decision Rule
If you are building a product used by external customers who sign in individually, use Delegated permissions. Each customer authenticates with their own Microsoft account, consents to your app's scopes, and your app operates on behalf of that authenticated user. Application permissions require a tenant administrator to pre-approve your app for their entire organization - a conversion-killing step in a self-serve SaaS funnel. The only exception is if you are building an internal enterprise tool deployed by your own IT team to your own tenant.
Full Walkthrough

Auth Code + PKCE: Step-by-Step Curl Examples

Here is the complete Microsoft Graph OAuth 2.0 Authorization Code flow with PKCE, from generating the code verifier to exchanging tokens. These are production-grade examples you can adapt directly to your stack.

Step 1 of 4 - Generate PKCE Parameters
Generate code_verifier and code_challenge
PKCE works by generating a random secret (code_verifier), then a SHA-256 hash of it (code_challenge). You send the challenge in step 2, and the verifier in step 4. Microsoft verifies they match, preventing code interception.
pkce_generate.py
import os, base64, hashlib # 1. Generate code_verifier (43-128 chars, URL-safe base64) code_verifier = base64.urlsafe_b64encode( os.urandom(32) ).decode('utf-8').rstrip('=') # 2. Generate code_challenge = BASE64URL(SHA256(code_verifier)) code_challenge = base64.urlsafe_b64encode( hashlib.sha256(code_verifier.encode('utf-8')).digest() ).decode('utf-8').rstrip('=') # Store code_verifier in session - you need it in step 4 # Send code_challenge in the authorization URL
Step 2 of 4 - Authorization Request
Build the /authorize URL and redirect the user
Redirect the user's browser to Microsoft's authorization endpoint. The user sees Microsoft's login page, authenticates, and consents to your app's scopes. Microsoft then redirects back to your redirect_uri with an authorization code.
client_id
Your Application (client) ID from Entra app registration
response_type
code - requests an authorization code
redirect_uri
Must exactly match a URI registered in your Entra app. URL-encoded.
scope
Space-separated list. Always include offline_access for refresh tokens.
state
Opaque value you generate. Returned unchanged in the callback. Use it to prevent CSRF and restore UI state.
code_challenge
The BASE64URL(SHA256(code_verifier)) value from step 1.
code_challenge_method
S256 - always use SHA-256, never plain
authorize_url.sh
# Build the authorization URL (format for readability) AUTH_URL="https://login.microsoftonline.com/common/oauth2/v2.0/authorize ?client_id=YOUR_CLIENT_ID &response_type=code &redirect_uri=https%3A%2F%2Fapp.com%2Fauth%2Fms%2Fcb &scope=Mail.ReadWrite%20Mail.Send%20offline_access &state=RANDOM_STATE_VALUE &code_challenge=YOUR_CODE_CHALLENGE &code_challenge_method=S256" # Redirect the user to $AUTH_URL # Microsoft handles login, MFA, consent screen # On success: redirect_uri?code=AUTH_CODE&state=...
Step 3 of 4 - Handle the Callback
Receive the authorization code at your redirect_uri
Microsoft redirects to your redirect_uri with a code query parameter. Validate the state parameter matches what you sent. The code expires in 10 minutes - exchange it immediately in step 4.
Step 4 of 4 - Token Exchange
Exchange the authorization code for access + refresh tokens
POST to the token endpoint with the code and your code_verifier. Microsoft returns an access token (valid for ~60-90 minutes) and a refresh token (long-lived). Store both securely.
token_exchange.sh
# Exchange authorization code for tokens curl -X POST "https://login.microsoftonline.com/common/oauth2/v2.0/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=YOUR_CLIENT_SECRET" \ -d "grant_type=authorization_code" \ -d "code=AUTH_CODE_FROM_CALLBACK" \ -d "redirect_uri=https://app.com/auth/ms/cb" \ -d "code_verifier=YOUR_CODE_VERIFIER" \ -d "scope=Mail.ReadWrite Mail.Send offline_access"
Returns: access_token, refresh_token, expires_in, scope
token_response.json
{ "token_type": "Bearer", "scope": "Mail.ReadWrite Mail.Send offline_access", "expires_in": 3600, "access_token": "eyJ0eXAiOiJKV1Qi...", "refresh_token": "0.ARoAi7W...", "id_token": "eyJ0eXAi..." }
Token Lifecycle

Refresh Token Handling: Rotation, Expiry, and Conditional Access

Microsoft Graph refresh tokens are long-lived but not permanent. Several conditions can invalidate them silently. Understanding these edge cases is what separates a production-grade Microsoft OAuth integration from one that breaks randomly for enterprise users.

90-day inactivity expiry
Microsoft refresh tokens expire after 90 days of inactivity. If a user does not use your app for 90 days, their refresh token becomes invalid and they must re-authenticate. There is no way to extend this without user interaction. Always handle invalid_grant errors gracefully and prompt re-auth.
Conditional Access policy changes
Enterprise tenants use Conditional Access policies (MFA requirements, device compliance, location restrictions). If a policy changes after a user authenticated, their refresh token may be invalidated on the next use. This is a customer-side decision - you cannot control or predict it. Always propagate auth errors back to users with a clear re-auth prompt.
Refresh token rotation
When you use a refresh token to get a new access token, Microsoft may issue a new refresh token. Always save the new refresh token from the response, replacing the old one. If you keep using the old refresh token after it has been rotated, you will eventually hit an invalid_grant error.
Admin revocation
A tenant admin can revoke all refresh tokens for a user or the entire organization at any time (e.g., when an employee leaves, or during a security incident). Your app receives invalid_grant. This is expected behavior - handle it by marking the linked account as needing re-authorization.
refresh_token.sh
# Refresh the access token using the stored refresh token curl -X POST "https://login.microsoftonline.com/common/oauth2/v2.0/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=YOUR_CLIENT_SECRET" \ -d "grant_type=refresh_token" \ -d "refresh_token=STORED_REFRESH_TOKEN" \ -d "scope=Mail.ReadWrite Mail.Send offline_access" # ALWAYS check for a new refresh_token in the response. # If present, replace the stored one immediately. # If you get invalid_grant, prompt the user to re-authenticate.
Troubleshooting

Common AADSTS Errors Decoded

Microsoft Graph OAuth errors follow a consistent AADSTS error code pattern. These are the most common ones you will encounter during development and production, with exact root causes and fixes.

Error Code What it means Root cause and fix
AADSTS65001 Consent has not been granted for one or more of the requested scopes The user has not consented to your app's scopes, or a tenant admin has blocked user consent for your app. Fix: Include prompt=consent in your authorization URL to force a fresh consent screen, or send the admin consent URL to the tenant admin.
Add prompt=consent or request admin consent
AADSTS50011 Redirect URI mismatch The redirect_uri in your request does not exactly match any registered redirect URI in your Entra app registration. Even a trailing slash difference causes this. Fix: Copy the exact URI from your Entra app registration and use it verbatim.
Fix: exact URI match in Entra app registration
AADSTS700016 Application not found in tenant The client_id does not exist in the tenant being authenticated against. Common when using a tenant-specific authority (/{tenant-id}) for a multi-tenant app. Fix: Use /common or /organizations authority for multi-tenant apps.
Fix: switch to /common or /organizations authority
AADSTS90099 Application has not been authorized in this tenant (consent_required) The app exists but has not been consented to in the user's tenant. Differs from AADSTS65001 in that the entire app is blocked, not just specific scopes. Fix: Send the admin consent URL to the customer's IT admin.
Fix: admin consent URL to customer tenant admin
AADSTS70011 Provided grant is invalid or expired The refresh token or authorization code has expired or been revoked. Authorization codes expire in 10 minutes. Refresh tokens expire after 90 days of inactivity or admin revocation. Fix: Prompt user to re-authenticate from the beginning of the OAuth flow.
Fix: prompt full re-authentication
AADSTS50076 MFA is required by Conditional Access policy The user's tenant requires multi-factor authentication for your app. This is a customer-side decision enforced by the tenant admin. Your app cannot bypass it. The user needs to complete MFA. If using the auth code flow, Microsoft will show the MFA prompt automatically in the browser. Issues arise in automated flows (client credentials) that cannot complete MFA.
Expected: user must complete MFA
AADSTS50020 User account from external identity provider does not exist in tenant The user is trying to authenticate with a personal Microsoft account into a tenant that only allows organizational accounts, or vice versa. Fix: Check your authority endpoint - if using /organizations, personal accounts cannot authenticate. Switch to /common if you need both.
Fix: switch authority to /common
AADSTS53003 Access blocked by Conditional Access policy The tenant's Conditional Access policy has blocked this authentication attempt entirely (e.g., blocked country, unmanaged device, blocked app). This is a customer-side decision. You cannot override it. Surface the error to the user and advise them to contact their IT admin.
Customer-side: advise user to contact IT admin
The Unipile Alternative

Skip 5 Weeks of OAuth Plumbing with Unipile

Everything in this guide - Entra app registration, authority endpoints, scope selection, PKCE, token rotation, AADSTS error handling - is engineering time that does not move your product forward. Unipile handles the entire Microsoft Graph OAuth stack as a managed service, so your team writes one API call instead of 500 lines of OAuth plumbing.

Building it yourself
Entra app registration and configuration
PKCE implementation and code challenge generation
Refresh token storage, rotation, and expiry handling
Admin consent flow for enterprise customers
AADSTS error handling and re-auth prompts
Client secret rotation before expiry
Conditional Access caveat handling per tenant
Separate OAuth stacks for Gmail and IMAP providers
Using Unipile
One POST to generate a hosted auth link
Unipile manages PKCE, tokens, and refresh automatically
Tokens never stored in your database - Unipile handles it
Same API for Microsoft, Gmail, and IMAP accounts
Webhook notifications when accounts need re-auth
Branded consent screen with your own Entra app credentials
Ship your email integration in hours, not weeks
How Unipile hosted Microsoft OAuth works
Your backend calls one endpoint to generate a hosted auth link. Your user clicks it, authenticates with Microsoft, and Unipile manages the full OAuth flow - Entra redirect, scope handling, token exchange, and refresh. The linked account is then available via Unipile's unified email API.
unipile_microsoft_oauth.py
import requests UNIPILE_API_URL = "https://apiXXX.unipile.com:XXX" UNIPILE_API_KEY = "your-api-key" # Step 1: Generate a hosted auth link for Microsoft response = requests.post( f"{UNIPILE_API_URL}/api/v1/hosted/accounts/link", headers={ "X-API-KEY": UNIPILE_API_KEY, "Content-Type": "application/json" }, json={ "type": "create", "providers": ["MICROSOFT"], "api_url": UNIPILE_API_URL, "expiresOn": "2026-12-31T23:59:59Z", # Optional: tie this link to a specific user "name": "user_id_123", # Optional: get notified when account is linked "notify_url": "https://app.yourproduct.com/webhooks/account-linked" } ) # Step 2: Redirect your user to this URL hosted_auth_url = response.json()["url"] # Example: https://account.unipile.com/[encoded-token] # Unipile handles: Entra redirect, consent screen, PKCE, # token exchange, refresh token storage, scope management # Step 3: After auth, use Unipile's email API to read/send emails = requests.get( f"{UNIPILE_API_URL}/api/v1/emails", headers={"X-API-KEY": UNIPILE_API_KEY}, params={"account_id": "linked-account-id"} )
Microsoft OAuth complete. Mailbox available via unified API.
Hosted auth flow
Unipile hosts the OAuth consent screen. Your users see a clean branded flow. No redirect URI maintenance, no CORS issues, no localhost vs production URL juggling.
Token management
Unipile stores and rotates Microsoft OAuth tokens on your behalf. Refresh token rotation, 90-day inactivity monitoring, and re-auth notifications are handled automatically.
Unified email API
After linking, Microsoft, Gmail, and IMAP mailboxes all respond to the same Unipile email endpoints. One integration serves all three providers.
Your own Entra credentials
Configure your own Microsoft Entra app credentials in the Unipile dashboard. Your users see your app name on Microsoft's consent screen, not Unipile's.
Webhook notifications
Receive a webhook when a linked account needs re-authentication (expired refresh token, admin revocation, Conditional Access change). Surface the re-auth prompt immediately in your product.
Read, send, and sync
After Microsoft OAuth via Unipile, you can read emails, send emails, sync threads, manage folders, and handle attachments - all via Unipile's unified email API. No separate Microsoft Graph client needed.
How Unipile operates
Unipile is an independent technical intermediary that acts on behalf of each authenticated user via Microsoft-issued OAuth tokens. When a user links their Outlook or Microsoft 365 mailbox, Unipile operates exclusively using that user's delegated permissions. Unipile is not affiliated with, endorsed by, or sponsored by Microsoft. We use the public Microsoft Graph API on behalf of authenticated end users. Each account operates within that user's own Microsoft Entra identity and their organization's Conditional Access policies.
FAQ

Frequently Asked Questions

The most common questions about Microsoft Graph OAuth for email integration, from scope selection to token lifecycle to enterprise consent flows.

01
Does Microsoft Graph OAuth work for both Outlook.com and Microsoft 365 accounts?
Yes. Using the /common authority endpoint, a single Microsoft Entra app registration handles authentication for both personal Outlook.com accounts and Microsoft 365 work/school accounts. The key is selecting "Accounts in any organizational directory and personal Microsoft accounts" when registering your app in the Azure portal.
02
What happens to OAuth tokens when a Microsoft 365 user leaves their company?
When an IT admin disables or deletes a user account in Microsoft Entra ID, all of that user's refresh tokens are immediately revoked. Your next token refresh attempt returns an invalid_grant error. Handle this gracefully: mark the linked account as requiring re-authentication and surface a clear re-auth prompt in your product. This is expected behavior - a customer-side decision beyond your control.
03
Is admin consent required to read Outlook emails via Microsoft Graph OAuth?
No, for standard Delegated permissions. Mail.Read, Mail.ReadWrite, and Mail.Send are user-consent scopes - individual users can approve them during the OAuth flow. Admin consent is only required for Application permissions or high-privilege scopes like User.Read.All. Some enterprise tenants configure policies that block all third-party app consent - that is a customer-side decision.
04
How long do Microsoft Graph refresh tokens last?
Refresh tokens expire after 90 days of inactivity. As long as your app uses the refresh token regularly (which happens automatically when refreshing access tokens before they expire every 60-90 minutes), the refresh token stays alive. Conditional Access policy changes, password resets, or admin revocation can invalidate them early. Always handle invalid_grant errors and replace old refresh tokens with the new one returned in each token response.
05
What is the difference between AADSTS65001 and AADSTS90099?
AADSTS65001: The user has not yet consented to one or more specific scopes. Fix: add prompt=consent to your authorization URL to force a fresh consent screen. AADSTS90099: The entire application has not been authorized in that tenant - the tenant admin needs to pre-approve your app. Send the admin consent URL to the customer's IT admin. Both errors are common in enterprise scenarios where tenants restrict user consent.
06
Can I use the same Entra app registration for both reading and sending emails?
Yes. Request both Mail.ReadWrite and Mail.Send in the same scope string. Note that Mail.ReadWrite and Mail.Send are separate scopes - having read/write access does not automatically grant send permission. Always include offline_access to ensure you receive a refresh token. See the email API page for implementation details.
07
Does Unipile store Microsoft OAuth tokens in my database?
No. When you use Unipile's hosted Microsoft auth flow, Unipile manages all OAuth tokens on your behalf. Your application never handles Microsoft access tokens or refresh tokens directly. You interact with linked accounts exclusively through Unipile's unified email API using your Unipile API key. This eliminates token storage, rotation, and security requirements from your own infrastructure.
08
Is Unipile affiliated with Microsoft?
No. Unipile is not affiliated with, endorsed by, or sponsored by Microsoft. Unipile is an independent technical intermediary that uses the public Microsoft Graph API on behalf of authenticated end users. Each integration operates via Microsoft-issued OAuth tokens under that user's own identity and their organization's Conditional Access policies. Microsoft Graph and Microsoft Entra are trademarks of Microsoft Corporation.
Still have questions? Our team can walk you through Microsoft Graph OAuth for your specific use case.
en_USEN