Skip to content
Frezz Tech logo with a futuristic AI robot and cloud circuitry, representing artificial intelligence, Microsoft Copilot, Azure AI, and cloud innovation. Frezz Tech logo with a futuristic AI robot and cloud circuitry, representing artificial intelligence, Microsoft Copilot, Azure AI, and cloud innovation. Frezz Tech

Real-world AI, Copilot and Azure architecture

Frezz Tech logo with a futuristic AI robot and cloud circuitry, representing artificial intelligence, Microsoft Copilot, Azure AI, and cloud innovation. Frezz Tech logo with a futuristic AI robot and cloud circuitry, representing artificial intelligence, Microsoft Copilot, Azure AI, and cloud innovation. Frezz Tech

Real-world AI, Copilot and Azure architecture

  • Home
  • Blog
  • About
  • Home
  • Blog
  • About
Close

Search

  • Home
  • Blog
  • About
Frezz Tech logo with a futuristic AI robot and cloud circuitry, representing artificial intelligence, Microsoft Copilot, Azure AI, and cloud innovation. Frezz Tech logo with a futuristic AI robot and cloud circuitry, representing artificial intelligence, Microsoft Copilot, Azure AI, and cloud innovation. Frezz Tech

Real-world AI, Copilot and Azure architecture

Frezz Tech logo with a futuristic AI robot and cloud circuitry, representing artificial intelligence, Microsoft Copilot, Azure AI, and cloud innovation. Frezz Tech logo with a futuristic AI robot and cloud circuitry, representing artificial intelligence, Microsoft Copilot, Azure AI, and cloud innovation. Frezz Tech

Real-world AI, Copilot and Azure architecture

  • Home
  • Blog
  • About
  • Home
  • Blog
  • About
Close

Search

  • Home
  • Blog
  • About
A user consents, a Foundry connection carries the user token to an MCP server and the server sees callerType user
GovernanceMicrosoft Foundry

From Workload to Person: OAuth Identity Passthrough for Your Own MCP Server in Microsoft Foundry

02.11.2026 14 Min Read
0

The same whoami server on Azure Container Apps, now with a signed in user in the token

Last verified against Microsoft Learn on 1 November 2026. Foundry changes quickly, so check the linked docs before you rely on a detail.

  • The server finally sees a person: With OAuth identity passthrough the whoami answer changes from application to user. The token carries a delegated scope and no app role.
  • The order of the setup matters: Foundry shows the redirect URL only after the connection exists. And an OAuth connection cannot be edited afterwards. A typo in the secret or the scopes means a new connection.
  • The server check stays the same: My demo writes scp and roles into one list. The same REQUIRED_ROLE setting now enforces a scope. No code change was needed for that.

The problem in plain language

In my last post the whoami server ran on Azure Container Apps and a Foundry agent called it with a Microsoft Entra token. The answer was callerType: application. The server saw a workload (the project managed identity or the agent identity) and no person. That is fine for many tools. It is not enough when the tool must know who asked.

So this post adds the user. Foundry calls the same server with a token of the signed in person. For that you need your own OAuth client. Microsoft calls this custom OAuth. The managed variant is not an option for your own server: Agent Service blocks tokens for Microsoft audiences from going to custom MCP servers.

Diagram with a client app and an API app in Microsoft Entra, a Foundry connection and an MCP server with arrows for the scope, the client ID and the user token
Two app registrations in Entra, one connection in Foundry and one server. The user consents once

What the docs say

The docs describe the connection fields and the consent flow (Set up authentication for MCP tools).

FieldRequiredNote
Client IDYesThe app registration of your OAuth client
Client secretOptionalDepends on your OAuth app
Auth URLYesEntra authorize endpoint of your tenant
Token URLYesEntra token endpoint of your tenant
Refresh URLYesWithout a separate refresh URL use the token URL
ScopesOptionalAdd offline_access for automatic refresh

Three more points from the docs matter for what follows. Consent is stored per tool (connection) per project. The user needs at least the role Foundry Agent Consumer on the project. And the Entra tenant of the user must match the tenant of the project.

Key insight: In the Foundry portal the scopes are separated by a space. The page about the Azure Developer CLI says commas. The two docs contradict each other. I used spaces in the portal and it worked.

Step 1: Add a delegated scope to your API

The API from the last post only has an app role for workloads. For a signed in user it needs a delegated scope (Expose a web API). The scope value ends up in the scp claim:

az rest --method PATCH \
  --uri "https://graph.microsoft.com/v1.0/applications(appId='<api-app-id>')" \
  --headers "Content-Type=application/json" \
  --body @scope.json
{
  "api": {
    "oauth2PermissionScopes": [
      {
        "adminConsentDescription": "Allows the app to call the whoami MCP tool on behalf of the signed in user.",
        "adminConsentDisplayName": "Call whoami MCP tool as user",
        "id": "<new guid>",
        "isEnabled": true,
        "type": "User",
        "userConsentDescription": "Allows the app to call the whoami MCP tool on your behalf.",
        "userConsentDisplayName": "Call whoami MCP tool as you",
        "value": "Mcp.Tools.Access"
      }
    ]
  }
}
Expose an API page of the API app registration with the scope Mcp.Tools.Access that admins and users can consent to and no authorized client applications
The scope can be consented by admins and users. No client application is pre-authorized, so users see a consent dialog

The type User means that users can consent themselves. I first tried az ad app update --set api.oauth2PermissionScopes=@file. It failed with Couldn't find 'api' in '' and I did not look for the reason. The call to Microsoft Graph worked right away.

Step 2: Create the client app registration

The client is the app that signs in the user. It holds the secret and later the redirect URL.

az ad app create --display-name <client-name> --sign-in-audience AzureADMyOrg --query appId -o tsv
az ad sp create --id <client-app-id>
az ad app permission add --id <client-app-id> --api <api-app-id> --api-permissions <scope-id>=Scope
az ad app credential reset --id <client-app-id> --display-name foundry-oauth --years 1 --query password -o tsv

The secret appears only once. Keep it out of your repo and out of files that the repo could pick up.

After permission add the CLI prints a hint to run az ad app permission grant. I did not. That command grants consent for everyone and the user would never see a consent dialog. Because the scope allows “Admins and users” the user can consent at the first call. If your tenant blocks user consent an admin has to grant it.

Step 3: Create the connection in the portal

In the Foundry portal go to Build, Tools, Connect a tool, Custom, Model Context Protocol. Select OAuth Identity Passthrough.

FieldValue
Namewhoami-mcp-oauth
Remote MCP Server endpointhttps://<your-app>/mcp
Client IDThe client app ID
Client secretThe secret from step 2
Auth URLhttps://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/authorize
Token URLhttps://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
Refresh URLThe same as the token URL
Scopesapi://<api-app-id>/Mcp.Tools.Access offline_access

After saving Foundry shows a redirect URL. For me it pointed to the consent service of Azure API Connections (global.consent.azure-apim.net/redirect/...) and not to my Container App.

Foundry connection form with the fields client ID, client secret, auth URL, token URL, refresh URL and scopes and a warning that OAuth connections cannot be updated
The warning at the top is the interesting part: an OAuth connection cannot be edited after it was created

Key insight: The portal says “OAuth doesn’t support updating the configuration. You can create a new connection or update to another auth option.” If you mistype the secret or a URL you create a new connection. A new connection gets a new redirect URL and you have to add that one to the client app as well.

Step 4: Add the redirect URL to the client app

In Entra add the URL as a redirect URI of the platform Web. With the CLI it looks like this.

az ad app update --id <client-app-id> --web-redirect-uris "<redirect-url>"
az ad app show --id <client-app-id> --query "web.redirectUris" -o json

--web-redirect-uris replaces the whole list. If you add a second connection later pass all URLs in one call.

Step 5: The first call and the consent

I created an agent with this connection and asked: “Call the whoami tool and show me the full result.”

The chat did not call the tool. It showed a consent request with the buttons “Open consent” and “Cancel” (the item type is oauth_consent_request).

Foundry playground chat with a sign in request for the connection and the buttons Open consent and Cancel
The agent stops at oauth_consent_request. No call reaches the server yet

“Open consent” opens a window outside of the chat. In my run it took three steps, one after the other:

  1. Entra, app 1 of 2: The client app asks for the scope “Call whoami MCP tool as you”, the text I wrote in step 1.
  2. Entra, app 2 of 2: A second dialog for the API app asks for basic profile access. I do not know why Entra splits it into two apps and I do not rely on it.
  3. Foundry, “Confirmation required”: A page with a phishing warning and a checkbox (“I have verified this request and trust the source”). Only after the checkbox the button “Allow access” works.
Microsoft consent dialog Permissions requested 1 of 2 apps for the client app with the permission Call whoami MCP tool as you
The scope from the API app appears in the consent dialog with the text written in step 1

Confirmation required page with a phishing warning, the connection name and a checkbox to confirm trust before the button Allow access works
Foundry asks you to confirm that you created the connection. Only after the checkbox the button Allow access works

After that the window says “Authentication successful” and the chat asks for the tool approval. The first time I ran this a page with “Authentication failed … Code … not found” showed up right after the consent. The chat still said “Sign in successful”. I do not know why the page appeared and I could not reproduce it.

After the approval the answer looked like this:

{
  "callerType": "user",
  "meaning": "A person is in the token. The server can apply that user's own permissions. This is what OAuth identity passthrough gives you.",
  "objectId": "56d16eb0...",
  "tenantId": "e44d86aa...",
  "clientApp": "55b52a07...",
  "userName": "a***",
  "delegatedScopes": [
    "Mcp.Tools.Access"
  ],
  "audience": "api://b68b92dd...",
  "issuer": "https://sts.windows.net/e44d86aa.../",
  "expiresAt": "2026-11-01T22:35:29+00:00"
}
Foundry playground with the whoami result showing callerType user, a masked object ID and the delegated scope Mcp.Tools.Access
The answer shows a user and a delegated scope. There is no app role

FieldWorkload (last post)User (this post)
callerTypeapplicationuser
userNamenonemasked user name
delegatedScopesnoneMcp.Tools.Access
appRolesMcp.Tools.ReadWrite.Allnone
clientAppthe calling applicationthe client app from step 2

Two details. The issuer is sts.windows.net again. The token is a version 1 token because I left the token version of the API at its default. The token URL is a version 2 endpoint and still the token is version 1. My server accepts both formats. And clientApp is now my own client app. It shows which application acts on behalf of the user.

Key insight: The server now sees a person. It sees it only because the user consented. Without the consent there is no user token.

What Entra stored

I checked the consent in Entra:

az rest --method GET --uri "https://graph.microsoft.com/v1.0/oauth2PermissionGrants?\$filter=clientId eq '<client-service-principal-id>'" --query "value[].{scope:scope,consentType:consentType}" -o table
ScopeConsentType
offline_accessPrincipal
Mcp.Tools.AccessPrincipal

Principal means the consent belongs to one user. No admin was involved. The two rows match the scopes in the connection.

The grants can be deleted with az rest --method DELETE on oauth2PermissionGrants/<id>. I did that to see the dialogs again. At the next call Entra asked again and created the same two rows. Deleting the grants therefore resets the user side and nothing else. The token that Foundry holds for an existing connection is a separate thing and I did not test whether it stops working.

Consent belongs to the connection

I created a second agent with the same connection and asked the same question. No sign in window appeared. Only the tool approval was left. That matches the docs: consent is stored per tool per project and not per agent.

Tool approval in the Foundry playground with the options Approve once, Always approve this tool and Always approve all tools
A second agent with the same connection only asks for the tool approval

The approval is a different thing from the consent. The default require_approval is always. Foundry asks before every call and offers three choices.

What happens when the user says no

I deleted my grants and tried the decline on a new connection (a connection that has no stored token yet). I did it at two places:

Cancel in the Entra dialog –> The external window shows error=consent_required with AADSTS65004: User declined to consent to access the app. The message comes from Entra. Afterwards the grants list was empty and the server log had no new request.

Authentication failed window with the error consent_required and AADSTS65004 User declined to consent to access the app
Cancel in the Entra dialog ends with consent_required and the code AADSTS65004

Cancel on the Foundry confirmation page –> I had already clicked Next and Accept in Entra. At that moment both grants existed. Then I cancelled the confirmation. The window says “Error: User did not consent service sign in.” The two grants stayed. So the consent in Entra and the confirmation of the connection are two separate decisions. Cancelling the second one does not undo the first.

Authentication failed window with the message User did not consent service sign in
Cancel on the Foundry confirmation page gives a different message and no AADSTS code

In both cases the playground showed “Sign in successful” in green and offered the consent request again.

Foundry playground with a green Sign in successful line after a declined consent and a new consent request with the buttons Open consent and Cancel
The green line only means that the window was closed – No tool ran

This matches the docs: opening or closing the window does not prove consent. A declined consent does not lock the connection. I clicked “Open consent” again. Entra did not ask a second time because the grants were still there. The confirmation page came directly. After “Allow access” and the approval the answer was callerType: user again.

Key insight: The green “Sign in successful” is not a proof. Look for the tool result. The server log tells it too: no POST /mcp and no audit line means no call.

Let the server enforce the scope

In the last post I added REQUIRED_ROLE to the demo. The setting also works for a scope. My verifier writes scp and roles into the same list and the SDK checks that list. I did not change any code:

az containerapp update --name <app> --resource-group <rg> --set-env-vars "REQUIRED_ROLE=Mcp.Tools.Access"
REQUIRED_ROLEToken of the userResult
Mcp.Tools.Accesshas the scope200
Mcp.Tools.Otherhas not this scope403

For the 403 Foundry shows the same long and general message as for a missing app role (see the [last post](TODO: URL of the hosting post)). It does not tell whether a role or a scope is missing. If you see it check both.

Foundry error message Access denied when connecting to the MCP server while invoking tool whoami with HTTP 403 Forbidden
The server required a scope the token did not have. The message does not name the scope

Does the token renew itself?

My first token expired at 22:35 UTC. A new conversation after that returned expiresAt: 2026-11-02T00:02:40+00:00. The same objectId and no new sign in window. The offline_access scope was part of my connection and the docs recommend it for this. I did not test the connection without it.

A word on audit logs

The demo never logs the token. Until now it did not log who called either. Uvicorn writes only the status code. A server that sees a person should leave a trace of that person, so I added one line per tool call:

2026-11-01 22:08:08,866 whoami.audit whoami called callerType=user objectId=56d16eb0... clientApp=55b52a07... scopes=Mcp.Tools.Access roles=-

The line shows the masked object ID and the scope. It never shows the token or the user name. A unit test checks that. For a workload token the same line shows callerType=application and the role in roles (I checked that in a unit test and not in Azure).

One limit: a 403 is not in the audit log. The SDK rejects the call before the tool runs. The log shows only the status code there. If you must audit rejected calls you need a check in front of the SDK.

What I did not test

  • A second user: The docs say consent is per user. I tested with one user.
  • The role Foundry Agent Consumer: The docs recommend it for end users. I am the owner of the project and did not need it.
  • Cancel in the second Entra dialog (2 of 2): I cancelled in the first Entra dialog and on the Foundry page. The second Entra dialog I did not cancel.
  • A stored token after deleting the grants: I deleted the grants only before using new connections. I did not test what an existing connection does after that.
  • Handling oauth_consent_request in your own client: The docs show Python and .NET samples. I used only the playground.
  • The delete path for the client app in the teardown script: I ran the script after the resources were already gone. It reported nothing to delete. The loop over both apps is written but I did not see it delete the client app.

Common pitfalls

SymptomLikely causeFix
The connection cannot be editedOAuth connections are fixed after creationCreate a new connection and add its redirect URL to the client app
You do not know the redirect URLFoundry shows it only after the connection existsCreate the client app first and add the URL after the connection
Scopes do not workSeparator in the fieldUse spaces in the portal. The CLI page says commas
az ad app update --set api.oauth2PermissionScopes fails with Couldn't find 'api' in ''I did not find the reasonSet the scope with a PATCH to Microsoft Graph
az suggests az ad app permission grantDefault hint after permission addSkip it. The user consents at the first call. Use it only if your tenant blocks user consent
The playground says “Sign in successful” but the tool does not runThe window was closed or the consent was declinedLook for the tool result and for POST /mcp in the server log. Click “Open consent” again
The agent answers without calling the toolSame chat and the model answers from the historyStart a new conversation
A page says Authentication failed ... Code ... not foundUnknownCheck the chat. For me the sign in still worked
The token has the issuer sts.windows.netToken version of the API is nullAccept both issuer formats on the server or set the token version
HTTP 403 Forbidden in a long general messageThe scope or the role is missingCheck which one the server requires

Clean up

This time there are two app registrations (API and client) and a connection. Delete the resource group and both apps. The demo repo has deploy/teardown.sh for that. It deletes both apps and the resource group and with --purge it frees the names again.

az group delete --name <rg> --yes --no-wait
az ad app delete --id <api-app-id>
az ad app delete --id <client-app-id>

The connection and the agents are part of the Foundry project in the resource group. Purge the Foundry resource as in the last post if you want to reuse the name.

Practical checklist

  1. Add a scope to your API: Type User so that users can consent.
  2. Create the client app: Service principal, permission for the scope and a secret that stays out of your repo.
  3. Create the connection: OAuth Identity Passthrough with spaces between the scopes and offline_access included.
  4. Add the redirect URL: Platform Web and the URL from the connection.
  5. Call the agent in a new conversation: Sign in once and approve the tool.
  6. Read the whoami answer: Confirm callerType: user and the scope.
  7. Enforce the scope on the server: REQUIRED_ROLE with the scope value.
  8. Log who calls: One masked line per tool call and never the token.
  9. Delete the resources: Both app registrations and the resource group.

Key takeaways

  1. For your own MCP server you need custom OAuth. Managed OAuth with a Microsoft audience is blocked (according to the docs).
  2. Create the client app first. The redirect URL comes after the connection.
  3. An OAuth connection cannot be edited. Create a new one.
  4. The user consents once per connection and project. A second agent does not ask again.
  5. The server sees a user token with scp and no app role.
  6. The same server setting enforces a scope. No code change needed.
  7. Foundry shows the same general 403 for a missing role and a missing scope.
  8. Log the caller identity yourself and never the token.
  9. The consent has three layers: Entra, the connection confirmation and the tool approval. A decline in one of them does not lock the connection.
  10. “Sign in successful” in the playground proves nothing. Check the tool result.

What comes next: So far the question was who calls the tool. The next question is which tools an agent may use at all. In the next post I look at governance for MCP tools with the AI gateway in Microsoft Foundry (Governance).

Sources

  • Set up authentication for Model Context Protocol (MCP) tools (Microsoft Learn)
  • Connect to Model Context Protocol servers (Microsoft Learn)
  • User delegation for tools in Microsoft Foundry (Microsoft Learn)
  • Expose a web API with delegated scopes (Microsoft Learn)
  • Add a redirect URI to an app registration (Microsoft Learn)
  • [Hosting Your Own MCP Server for Microsoft Foundry on Azure Container Apps](TODO: URL) (frezz-tech.de)
  • Which Agent, on Whose Behalf? Authenticating MCP Tools in Microsoft Foundry (frezz-tech.de)
  • foundry-mcp-auth-whoami-demo (GitHub)

Tags:

AI AgentsAuthenticationAzure Container AppsIdentity PassthroughMCPMicrosoft EntraMicrosoft FoundryOAuth
Author

Alexander Dierkes

Follow Me
Other Articles
A Foundry agent sends an Entra token to an MCP server on Azure Container Apps which answers 200 OK with the app role and 403 Forbidden without it
Previous

Hosting Your Own MCP Server for Microsoft Foundry on Azure Container Apps

No Comment! Be the first one.

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Latest Posts

  • From Workload to Person: OAuth Identity Passthrough for Your Own MCP Server in Microsoft Foundry
  • Hosting Your Own MCP Server for Microsoft Foundry on Azure Container Apps
  • Which Agent, on Whose Behalf? Authenticating MCP Tools in Microsoft Foundry
  • Which Identity Does Your Foundry Agent Really Use?
  • Running a 70B model on a laptop with AirLLM: what it costs in time and where Microsoft Foundry fits

AI Agents Authentication Azure Azure Container Apps Azure OpenAI Foundry Local Governance Identity Passthrough Local AI MCP Microsoft Entra Microsoft Foundry OAuth

Resources

About | Imprint

Disclaimer

Opinions expressed here are my own and may not reflect those of others. Unless I'm quoting someone, they're just my own views.

© 2026 Frezz Tech - All rights reserved by Alexander Dierkes
Independent technology blog. Not affiliated with Microsoft.