Skip to content

Commit ee74caf

Browse files
lilyjmaCopilot
andcommitted
Add OBO auth documentation: consent, code walkthrough, troubleshooting
- Update intro to mention hello_tool_with_auth and OBO flow - Add Step 4 (consent) and Step 5 (connect) to deploy instructions - Add 'Calling Microsoft Graph with OBO' code walkthrough section - Add 'Consent authoring' section explaining user vs admin consent - Expand troubleshooting with OBO-specific errors and solutions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2e50248 commit ee74caf

1 file changed

Lines changed: 93 additions & 5 deletions

File tree

src/FunctionsMcpTool/README.md

Lines changed: 93 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# FunctionsMcpTool — Remote MCP Server on Azure Functions (Python)
22

3-
This project is a Python Azure Function app that exposes multiple MCP (Model Context Protocol) tools as a remote MCP server. It includes tools for snippets, QR code generation, structured metadata, batch operations, and more.
3+
This project is a Python Azure Function app that exposes multiple MCP (Model Context Protocol) tools as a remote MCP server. It includes tools for snippets, QR code generation, badges, structured metadata, batch operations, and a **hello with auth** tool that demonstrates the On-Behalf-Of (OBO) flow to call Microsoft Graph as the signed-in user.
44

55
> **Note:** MCP resources are in the [FunctionsMcpResources](../FunctionsMcpResources/) project, and prompts are in the [FunctionsMcpPrompts](../FunctionsMcpPrompts/) project.
66
@@ -120,7 +120,7 @@ This also becomes the resource group name.
120120

121121
### Step 3: Provision and deploy
122122

123-
By default, OAuth-based authentication is enabled using the [built-in MCP auth feature](https://learn.microsoft.com/azure/app-service/configure-authentication-mcp?toc=/azure/azure-functions/toc.json&bc=/azure/azure-functions/breadcrumb/toc.json) with Microsoft Entra as the identity provider.
123+
This project requires OAuth-based authentication through the [built-in MCP auth feature](https://learn.microsoft.com/azure/app-service/configure-authentication-mcp?toc=/azure/azure-functions/toc.json&bc=/azure/azure-functions/breadcrumb/toc.json) with Microsoft Entra as the identity provider, and it is enabled by default. Do not disable authentication for this project.
124124

125125
Configure VS Code as an allowed client application for Microsoft Entra:
126126

@@ -140,11 +140,25 @@ Deploy the project. When prompted, pick your subscription and an Azure region.
140140
azd up
141141
```
142142

143-
### Step 4: Connect to the remote MCP server
143+
### Step 4: Consent to the application
144144

145-
Open **`.vscode/mcp.json`** and click **Start** above **`remote-mcp-function`**. You'll be prompted for `functionapp-name` — find it in your `azd` command output or the `.azure/<env>/.env` file. Since authentication is enabled, you'll also be prompted to sign in with Microsoft.
145+
The `hello_tool_with_auth` tool requires consent for delegated permission to access Microsoft Graph. For testing, you can grant consent just for yourself by logging into the application in a browser. See [Consent authoring](#consent-authoring) for how you would handle this for production scenarios.
146146

147-
> **Tip:** Click **More... → Show Output** above the server name to see request/response details.
147+
Navigate to the `/.auth/login/aad` endpoint of your deployed function app. For example, if your function app is at `https://my-mcp-function-app.azurewebsites.net`, navigate to:
148+
149+
```
150+
https://my-mcp-function-app.azurewebsites.net/.auth/login/aad
151+
```
152+
153+
Sign in with your Azure subscription email and accept the permissions prompt. This completes the consent flow for you.
154+
155+
### Step 5: Connect to the remote MCP server
156+
157+
Open **`.vscode/mcp.json`** and click **Start** above **`remote-mcp-function`**. You'll be prompted for `functionapp-name` — find it in your `azd` command output or the `.azure/<env>/.env` file. You'll also be prompted to authenticate with Microsoft — click **Allow** and sign in.
158+
159+
> **Tip:** A successful connection shows the number of tools the server exposes. Click **More... → Show Output** above the server name to see request/response details.
160+
161+
> If you run into issues, see the [Troubleshooting](#troubleshooting) section below.
148162
149163
### Redeploy and clean up
150164

@@ -190,11 +204,85 @@ def generate_qr_code(text: str) -> ImageContent:
190204
)
191205
```
192206

207+
### Calling Microsoft Graph with the On-Behalf-Of flow (`hello_tool_with_auth`)
208+
209+
The `hello_tool_with_auth` tool (in [`hello_tool_with_auth.py`](hello_tool_with_auth.py)) demonstrates how to call a downstream API (Microsoft Graph) **as the signed-in user** using the On-Behalf-Of (OBO) flow.
210+
211+
**Local development** falls back to your local developer identity (Azure CLI, azd, etc.):
212+
213+
```python
214+
if is_local:
215+
credential = ChainedTokenCredential(
216+
AzureCliCredential(),
217+
AzureDeveloperCliCredential(),
218+
)
219+
else:
220+
credential = _build_obo_credential(context)
221+
```
222+
223+
**In production**, the `_build_obo_credential` function exchanges the user's auth token for a Microsoft Graph token using three pieces of information:
224+
225+
1. **The user's bearer token** — extracted from the `X-MS-TOKEN-AAD-ACCESS-TOKEN` header (or `Authorization` fallback)
226+
2. **The user's tenant ID** — decoded from the `X-MS-CLIENT-PRINCIPAL` header
227+
3. **A client assertion** — obtained from a managed identity with a federated credential, proving the app's identity without a client secret
228+
229+
```python
230+
def _build_obo_credential(context):
231+
# Extract headers from MCP context
232+
headers = context.get("HttpTransport", {}).get("Headers", {})
233+
234+
user_token = headers.get("X-MS-TOKEN-AAD-ACCESS-TOKEN", "")
235+
tenant_id = ... # decoded from X-MS-CLIENT-PRINCIPAL
236+
237+
managed_identity = ManagedIdentityCredential(client_id=federated_mi_client_id)
238+
239+
def client_assertion_func():
240+
return managed_identity.get_token("api://AzureADTokenExchange/.default").token
241+
242+
return OnBehalfOfCredential(
243+
tenant_id=tenant_id,
244+
client_id=client_id,
245+
client_assertion_func=client_assertion_func,
246+
user_assertion=user_token,
247+
)
248+
```
249+
250+
The resulting credential is then used to call Microsoft Graph `/me` and greet the user by name:
251+
252+
```python
253+
token = credential.get_token("https://graph.microsoft.com/.default")
254+
async with aiohttp.ClientSession() as session:
255+
async with session.get(
256+
"https://graph.microsoft.com/v1.0/me",
257+
headers={"Authorization": f"Bearer {token.token}"},
258+
) as resp:
259+
me = await resp.json()
260+
return f"Hello, {me['displayName']} ({me['mail']})!"
261+
```
262+
263+
## Consent authoring
264+
265+
In the steps described for this example, you consented to the application by signing into it in a browser. This allowed the application to request delegated permissions to the Microsoft Graph. There are two main ways that consent can be handled:
266+
267+
- **User consent** — This is the approach used in the example above. Each user signs into the application and consents to the permissions requested. They can only do this for themselves, unless they are a tenant administrator with the ability to consent on behalf of others. In this sample, user consent is appropriate because it allows you to quickly test things without impacting other users. However, the way user consent is authored in this sample does not reflect how you would typically do it in a production scenario. This is described in more detail below.
268+
269+
- **Admin consent** — A tenant administrator can consent to the application on behalf of all users when they sign in and review the permissions. Once this is done, individual users can sign in without needing to consent themselves. This approach is more scalable and ensures that all users can access the application without running into consent issues. For the purposes of a sample, admin consent is not appropriate, but it is a great choice for production scenarios.
270+
271+
The user consent approach for this sample is a separate login because the sample uses Visual Studio Code as the client. Although Visual Studio Code is pre-authorized to our application, that only creates consent for the user to call the MCP server. It doesn't create consent for the MCP server to call the Microsoft Graph on behalf of the user. When we log into the application directly, we request Microsoft Graph permissions as part of a combined consent experience.
272+
273+
The main difference is that because Visual Studio Code is using a single sign-on flow, it only requests a token for the MCP server. It does not present an opportunity for the user to interactively consent to any permissions needed for or by the MCP server. If you built a client that used an interactive login of some kind, you could have it all handled entirely by that client. It would not be necessary to have a separate browser login.
274+
275+
See [Overview of permissions and consent in the Microsoft identity platform](https://learn.microsoft.com/en-us/entra/identity-platform/permissions-consent-overview) for additional information on how Entra ID handles consent.
276+
193277
## Troubleshooting
194278

195279
| Problem | Solution |
196280
|---------|----------|
197281
| Connection refused locally | Ensure Azurite is running (`docker run -p 10000:10000 ...`) |
198282
| API version not supported by Azurite | Add `--skipApiVersionCheck` flag to the Azurite command, or pull the latest image |
283+
| `hello_tool_with_auth` fails locally | Ensure you're signed in with `az login` or `azd auth login` |
284+
| OBO errors in production | Verify that consent has been granted (see Step 4) and that the Entra app registration is configured correctly |
285+
| `An error occurred invoking 'hello_tool_with_auth'` right after `azd up` | Restart the function app: `az functionapp restart -g <resource-group> -n <function-app-name>`. The OBO flow signs a client assertion with the user-assigned managed identity via a federated identity credential (FIC). Right after provisioning, the auth runtime can hold a stale signing credential while the FIC propagates in Entra. Check **Application Insights > Logs** for `AADSTS50013: Assertion failed signature validation` to confirm. |
286+
| Generic "An error occurred invoking" with no details | Check **Application Insights > Logs** and query `exceptions \| where timestamp > ago(1h) \| project timestamp, outerMessage, innermostMessage` to find the actual error. |
199287
| `AttributeError: 'FunctionApp' object has no attribute 'mcp_resource_trigger'` | Python 3.13 is required. Verify with `python3 --version`. |
200288
| `azd up` provision succeeded but deploy failed | Transient error — run `azd deploy` again |

0 commit comments

Comments
 (0)