In part 1 we intentionally avoided authentication and focused on learning MCP fundamentals. In part 2 we wired a no-auth MCP server into a Copilot Studio agent.
Part 3 is where things get “real enterprise”: the MCP server is no longer calling a public API. It’s calling Microsoft Graph, and Graph requires user-delegated authorization to retrieve Message Center messages via the Microsoft Graph API /admin/serviceAnnouncement/messages.
That combination drives a specific OAuth pattern:
In this post, “your MCP API” means the protected API surface of your MCP server (specifically POST /mcp), represented by a Microsoft Entra app registration that you “Expose an API” for. Practically, it’s the resource clients request a token for, typically with an audience like api://<your-app-client-id> for your server.
- The client (an agent or script) sends a user token meant for your MCP API.
- The server exchanges that token for a Microsoft Graph delegated access token using On-Behalf-Of (OBO).
- The server calls Graph and returns results as MCP tool output.
We’ll use the mcp-message-center-server repo as the reference model for the discussion.
TL;DR: If you just want the hands-on path, jump to Try it yourself.
Why OBO matters for MCP servers
If your MCP server calls Microsoft Graph with delegated permissions, it’s acting as a middle-tier service in a multi-hop authentication chain.
Here’s the key constraint that forces OBO:
- When a client calls your MCP server, it presents an access token representing the user.
- That incoming token is scoped for your MCP API (audience = your resource, e.g.,
api://<clientId>). - OAuth2 access tokens are not reusable across different resource servers.
- A token minted for your MCP API cannot be sent to Microsoft Graph, because Graph will reject it as the wrong audience.
The On-Behalf-Of (OBO) flow solves this by letting the MCP server exchange the incoming user token for a new access token:
- The new token is scoped for Microsoft Graph.
- The call still happens as the original user, preserving user identity and delegated permissions.
That creates the trust chain you want in an enterprise MCP deployment:
- The user authorizes the client.
- The client calls the MCP server on behalf of the user.
- The MCP server calls Microsoft Graph on behalf of that same user.
Without OBO, you’re pushed into two architectural dead ends:
- Server uses app-only auth to Graph: you lose user context (and often break least-privilege expectations for admin data).
- Client obtains Graph tokens directly: you break the middle-tier pattern and move Graph complexity (and token handling risk) into every client. This works for calling Graph directly, but the MCP server cannot validate that token as authentication because the token’s audience is Graph, not the MCP server. In the reference server, Graph tokens presented as the caller credential are rejected by default (confused-deputy guardrail) unless you explicitly opt in for local testing. This prevents a “confused deputy” scenario where a token meant for one service (Graph) is misused to access another (MCP server).
OBO is what preserves user identity across service boundaries while still respecting the OAuth principle that tokens are valid only for their intended resource.
- Client gets token scoped for MCP server (audience = MCP server)
- MCP server validates that token (proving the client is authorized to call it)
- MCP server exchanges it via OBO for a Graph token (audience = Graph)
- Maintains security boundaries while preserving user identity
Microsoft Message Center MCP server
This server mcp-message-center-server exposes one tool:
getMessages: queries Microsoft Admin Center Message Center messages via Microsoft Graph:/admin/serviceAnnouncement/messages
It also exposes a few key HTTP endpoints:
GET /healthz(health)POST /mcp(MCP JSON-RPC endpoint)GET /discover(andGET /.well-known/openid-configuration) for discovery/authorize+/tokenproxy endpoints (used by interactive and PKCE flows)
ℹ️
PKCE (Proof Key for Code Exchange) is an OAuth 2.0 protection for the authorization-code flow, designed for “public clients” that can’t safely store a client secret (like local tools, VS Code extensions, or scripts running on a dev machine). In PKCE, the client generates a one-time code verifier and sends only a derived code challenge during
/authorize. Later, when exchanging the auth code at/token, the client must present the original verifier. If an attacker steals the auth code from the redirect, they still can’t redeem it without the verifier. In this project, PKCE shows up when a client uses the server’s/authorize+/tokenproxy endpoints (for example, the repo’s PKCE smoke test). It is not required for the normal OBO path where the client callsPOST /mcpwithAuthorization: Bearer <MCP API token>.
Step 1 — Understanding the authentication token types
The server model makes a clear distinction between two token types:
MCP API user token (what the client sends)
- Audience: your MCP server API (example:
api://<clientId>) - Where used: sent by the client to
POST /mcpinAuthorization: Bearer <token>header. - Purpose: proves the user to your server
Microsoft Graph access token (what the server uses)
- Audience: Microsoft Graph
- Where used: sent by the MCP server to Microsoft Graph
- Purpose: authorizes the server to call Graph as the user (delegated)
If you only remember one thing:
Clients should authenticate to the MCP server. The MCP server should authenticate to Graph.
Step 2 — The OBO flow (what’s actually happening)
In OAuth terms, OBO is a token exchange:
- The client gets a user token for the MCP API (audience = your MCP API).
- The client calls
POST /mcpwith that bearer token. - The server validates the inbound token, then calls Entra to exchange it for a Graph token.
- The server calls Graph with the exchanged token.
At a high level, the runtime flow looks like:
POST /mcpreceives MCP JSON-RPC- Server checks
Authorization: Bearer <MCP API token> - Server performs OBO to get Graph token
- Server calls
GET https://graph.microsoft.com/v1.0/admin/serviceAnnouncement/messages?... - Server returns tool output back to the MCP client
Why this is the right pattern for agents
This is a good match for agent ecosystems because:
- The agent host (client) can be “thin” on Graph knowledge.
- Authorization remains user-delegated, so Graph enforces user role requirements (e.g., Message Center Reader).
- You can implement centralized controls (logging, throttling, input validation, safe defaults) at the MCP server.
Step 3 — App registration models (single-app vs two-app)
The reference repo supports two common setups:
Option A (simplest): single app registration (one app, two roles)
In this model, a single Microsoft Entra app registration serves as both:
The MCP API resource
- The caller obtains a user token whose audience is your MCP API.
- Common convention: set Application ID URI to
api://<clientId>.
The confidential client for OBO
- The server authenticates as the app to Entra to perform OBO.
- For local/dev, this can be a client secret.
- For production, prefer certificate-based auth (see Key Vault section).
This “single app” model reduces moving parts and is great for learning.
Option B (recommended for agents): two app registrations (client app + server/API app)
For many agent scenarios (including declarative agents), it’s cleaner to separate responsibilities:
Server/API app registration (resource)
- Exposes the MCP API scope (commonly
access_as_user) and representsPOST /mcp. - The inbound bearer token audience is your MCP API (for example,
api://<server-app-id>). - Uses delegated permissions to Microsoft Graph (e.g.,
ServiceMessage.Read.All) - validating user roles for Message Center access.
- Exposes the MCP API scope (commonly
Client app registration (OAuth client)
- Used for interactive login/PKCE and represents the client identity.
- Requests tokens for the server/API scope (for example,
api://<server-app-id>/access_as_user).
The repo includes PowerShell automation to create/configure both app registrations:
scripts/CreateServerAppRegMCP.ps1scripts/CreateClientAppRegMCP.ps1
Step 4 — Why proxy /authorize + /token endpoints exist
MCP clients are not all the same. Some can do interactive OAuth directly against Entra, some can’t. Some need PKCE. Some need a consistent “discovery + authorization” surface.
The Message Center MCP server provides /authorize and /token endpoints that act as a controlled OAuth proxy:
- They forward to Entra for the same app registration used by the server.
- They allow tooling and scripts to run standard OAuth flows (especially PKCE) without every client needing bespoke Entra wiring.
- They let the server enforce “safe defaults” and guardrails.
Redirect safety: allowlist is not optional
An OAuth proxy can become dangerous if it accepts arbitrary redirect_uri values.
That’s why the model server enforces a redirect URI allowlist (prefix-based) via MCP_OAUTH_REDIRECT_URI_PREFIXES.
Defaults include common safe cases:
- VS Code loopback (
http://127.0.0.1,http://localhost) - Copilot / Teams declarative agent redirect (
https://teams.microsoft.com/api/platform/v1.0/oAuthRedirect)
If your client uses a different redirect URI, you must explicitly allow it.
Try it yourself
This is the PowerShell-first path that validates:
- the server is running
/authorize+/tokenproxy works with PKCE- the end-to-end OBO path works (MCP API token → Graph token → Message Center)
- bypass paths work (and how to disable them in production)
Prerequisites
- Node.js 20+
- PowerShell 7+ (
pwsh) - Azure CLI (
az) authenticated (az login) - A Microsoft Entra app registration configured as described below
- The signed-in user must have the right Microsoft 365 role for Message Center access (e.g., Message Center Reader)
Create and configure an Entra app registration for the MCP server
For the purposes of this blog post we only need the server Application registration. In the next part of the series we will integrate this server with a client and at that time we will register the client application registration.
Follow Option B to create two app registrations one for the client and the other for the server/API.
Run the server app registration script:
| |
Clone the repo and run the server
| |
Minimum .env.local values for local dev OBO:
GRAPH_TENANT_ID=<your-tenant-guid>GRAPH_CLIENT_ID=<your-app-client-id-guid>GRAPH_CLIENT_SECRET=<client-secret>(local/dev simplest)
Then run the server:
| |
In a new terminal, verify health:
| |
Entra app registration - MCP OBO Server configuration
The following are the details regarding the server side app registration. This is all completed by the CreateServerAppRegMCP.ps1 script.
Expose an API (defines “your MCP API”)
- Set the Application ID URI (recommended):
api://<clientId> - Add a delegated scope so clients can request a token for your MCP API:
- Scope name:
access_as_user - Who can consent?: Admins and users
- Admin consent display name:
Access MCP server as signed-in user - Admin consent description:
Allows the app to call the MCP server API as the signed-in user. - User consent display name:
Access MCP server as signed-in user - User consent description:
Allows the app to call the MCP server API as the signed-in user.
- Scope name:
Why this matters:
- Your scripts/clients need some delegated scope to request on your MCP API.
- In current Azure CLI usage, request a token for the specific scope using
--scope api://<clientId>/access_as_user.
- Set the Application ID URI (recommended):
Microsoft Graph delegated permissions (what OBO will request)
- API permissions → Microsoft Graph → Delegated:
- Add
ServiceMessage.Read.All
- Add
- Click Grant admin consent (common cause of OBO failure if skipped).
- Note: Graph will still enforce the user’s Microsoft 365 role (e.g., Message Center Reader).
- API permissions → Microsoft Graph → Delegated:
Authentication (redirect URIs used by
/authorize+/tokenproxy)- Add the redirect URI your client uses (examples):
- Local loopback for PKCE smoke testing:
http://localhost:8400/(default used by the repo’sSmokeTokenProxyPkce.ps1). - Other loopback patterns:
http://localhost:<port>orhttp://127.0.0.1:<port>.
- Local loopback for PKCE smoke testing:
Here are the default redirect URIs allowed by the server’s OAuth proxy:
http://127.0.0.1http://localhosthttps://teams.microsoft.com/api/platform/v1.0/oAuthRedirect
- Add the redirect URI your client uses (examples):
Certificates & secrets (confidential client credentials for OBO)
- Local/dev (simplest): create a client secret and set
GRAPH_CLIENT_SECRET. - Production (recommended): use a certificate (public cert uploaded to the app registration, private key stored in Key Vault). Not required for this dev/test path.
- Local/dev (simplest): create a client secret and set
Smoke test the OAuth proxy endpoints with PKCE
From the repo root:
| |
This flow exercises /authorize + /token and helps validate redirect URI allowlisting and PKCE behavior.
The script returns the obtained access tokens if successful.
Get an MCP API user token (audience = your MCP API)
From the repo root:
| |
Call the MCP tool using OBO (recommended path)
This validates the full chain:
- client authenticates to MCP API
- server exchanges token via OBO
- server calls Graph
| |
Congratulations! You’ve proven the core production pattern.
Production lock-down behavior (important)
If NODE_ENV=production, the model server rejects bypasses unless you explicitly enable:
ALLOW_MCP_ACCESS_TOKEN_ARG=true
This is a deliberate safety feature: it prevents “just paste a Graph token” from creeping into production workflows.
Troubleshooting
AADSTS65001 / consent_required
You haven’t granted consent yet. Run:
| |
401 Unauthorized from the MCP server
- You didn’t send an MCP API bearer token and
MCP_REQUIRE_AUTH=true, or - Your bearer token is invalid/expired.
401 from Microsoft Graph
Common causes:
- OBO token exchange failed (missing delegated permission / missing admin consent)
- Your app registration isn’t configured with the right audience (
api://<clientId>) or token settings
403 from Microsoft Graph
Your user likely lacks the required Microsoft 365 admin role for Message Center access (e.g., Message Center Reader).
Production bypass error (accessToken_disabled)
You’re trying to use a dev/test bypass in production mode. Use the OBO path instead, or explicitly opt-in:
ALLOW_MCP_ACCESS_TOKEN_ARG=true
Summary
In Part 3 you’ve added the authentication story for an MCP server calling Microsoft Graph using On-Behalf-Of (OBO):
- The client authenticates to your MCP API
- The server uses OBO to obtain a Graph delegated token
- The server calls Graph and returns results as MCP tool output
/authorize+/tokenproxy endpoints support standardized OAuth flows with guardrails- Dev/test bypass paths exist, but are explicitly controlled and disabled by default in production
Next up (in Part 4): we’ll use this MCP server in an agent built with the Microsoft 365 Agents Toolkit.
