A React chat needs to send a message to a Azure Foundry agent. That does not mean React needs a Foundry credential. Putting a cloud token in frontend configuration creates a second authentication problem: the browser now holds both the customer's application identity and a credential for the agent platform.
A backend for frontend (BFF) separates those concerns. The browser authenticates to the application. The BFF validates that identity, acquires its own Azure token, and constructs the downstream request. The useful change is not hiding a URL; it is deciding which process owns each credential.
This article examines that boundary in a Python banking-assistant prototype adapted from Microsoft's banking-assistant sample. The dedicated Identity service and DB-free Responses BFF are this project's adaptations, not claims about the upstream sample. The reference code revision identifies the implementation snapshot; all implementation links below are pinned to it.
The central change: construct headers, do not forward them
The browser's application bearer must not become the agent's Azure bearer. The BFF creates a new header set from a verified principal and a server-owned credential.
This illustrative Python fragment summarizes the Foundry-mode policy. It is not a complete endpoint or runnable authentication implementation; credential, settings, and internal_identity are already initialized server-side:
token = await credential.get_token(settings.responses_token_scope)
headers = {
"Accept": "text/event-stream",
"Authorization": f"Bearer {token.token}",
"x-ms-user-identity": internal_identity,
}The owning implementation builds the signed identity from the authenticated user, not a browser-supplied identity header. The configured token scope is https://ai.azure.com/.default.
These two outbound values answer different questions:
Azure bearer: may this server identity call the configured Foundry endpoint?
Signed application identity: which verified customer is this application request acting for?
The second value is a repository-specific HMAC envelope. Its header name does not make it standard Foundry delegation, MCP OAuth, or proof that every hosted deployment forwards it correctly.
One chat request, two identity systems

Figure 1. Chat admission and credential separation in the implemented BFF. This is an architectural sequence, not a capture of hosted end-to-end execution.
The BFF validates the application JWT before the current-state check. Accepted Foundry-mode requests carry both a server Azure bearer and a custom signed customer-identity envelope. Azure token acquisition is internal to the BFF and omitted as a separate lifeline.
This doc-inline sequence uses the default Diagram Design palette and static SVG, with system font fallbacks. It omits login, MCP calls, direct financial REST reads, and continuation internals to focus on chat admission. Hosted custom-header delivery and browser visual acceptance remain unverified.
The Identity service owns password verification, persisted profiles, and application JWT issuance. The BFF owns neither passwords nor database queries. Before admitting a protected request, it validates the JWT and performs an uncached Identity check. Current lifecycle state, identity version, role, and customer claims must agree.
Only customers may enter chat. Staff identities can use their allowlisted administrative surfaces, but an operator or administrator token is not permission to invoke the customer agent. Invalid or stale credentials produce a controlled authentication error; Identity unavailability fails closed rather than becoming anonymous access.
This boundary applies to chat, not every browser request. Financial reads still go directly from the frontend to Account and Transaction REST APIs with the application JWT. Those services enforce resource ownership. Authenticating a customer at the BFF does not authorize an arbitrary account identifier, and prompt instructions cannot replace that check.
What must already exist
This is an implementation explanation, not a deployment tutorial. The reviewed BFF requires Python 3.11 or later and pins azure-identity 1.25.3, FastAPI 0.141.1, and HTTPX 0.28.1 in its manifest.
Foundry mode needs a trusted Responses endpoint, an appropriately authorized server identity, and the application identity-signing configuration. The credential factory chooses asynchronous AzureCliCredential for the development profile and ManagedIdentityCredential otherwise, optionally selecting a user-assigned identity by client ID. It does not use DefaultAzureCredential.
The Azure credential is created once for the application's lifespan and closed at shutdown. Credential reuse is not a token-cache guarantee: Microsoft Learn notes that direct AzureCliCredential.get_token calls do not cache acquired tokens. This development path requests a token per call. Selecting managed identity avoids embedding an Azure client secret in the browser or BFF configuration, but still requires managed-identity support and Azure permissions.
Local mode is deliberately different: it sends the signed envelope as x-agent-user-id and does not construct an Azure credential or upstream Azure Authorization header. The downstream signing secret is separate from the secret protecting Identity introspection. Neither belongs in frontend environment variables.
Inspect allowed and denied behavior without calling a model
At revision 05ea212e73a42bf368ecc0e2436f4c49e066ea41, the reviewed BFF files match the public source. The following command was executed from the repository root using the existing BFF environment:
$env:OTEL_SDK_DISABLED = 'true'
uv run --directory app\responses-bff --no-sync python -m pytest tests\test_auth.py tests\test_responses.py tests\test_responses_identity.py tests\test_responses_transport.py -k 'not w3c_context and not independent_traces' -qObserved result: 120 passed, 7 deselected. These are in-process tests with synthetic identities, mock HTTP transports, and a fake Azure credential. No browser, Azure login, hosted agent, or billable model call was involved.
Evidence | Supported conclusion | Not established |
|---|---|---|
Foundry-mode header test | Requested scope and server bearer are used; signed customer identity is attached | Real Azure permissions or hosted header delivery |
Local-mode test | Local chat does not initialize an Azure credential | Hosted parity |
Current-state and staff tests | Rejected identities never reach the mocked agent | Live database or browser isolation acceptance |
Credential-failure test | Token acquisition failure becomes controlled HTTP 503 | Cloud outage recovery |
The initial run included seven tracing tests that failed with missing traceparent while telemetry was disabled. The narrowed run excludes those assertions; it is not a full-suite success or tracing validation. The Responses tests and credential-failure test make the boundary inspectable without cloud execution.
The tradeoff and the remaining gate
A BFF adds a network hop, streaming lifecycle responsibilities, and an availability dependency on Identity. Uncached checks prioritize current authorization state over fewer calls. They do not retroactively cancel an already admitted stream.
The browser still holds a sensitive application token, so frontend protections, TLS, and careful logging remain necessary. The BFF does not make the upstream endpoint private, replace Azure RBAC, or guarantee business-service ownership checks. This prototype's HS256 verification also means verifier-secret holders are trusted token issuers, an explicit limitation in ADR 0006.
Before claiming deployed support, verify that the hosted transport delivers the custom identity envelope to the agent and that downstream tools enforce customer isolation. Offline header assertions do not close that gate.
The transferable pattern is small: keep the Azure credential server-side, validate the application's customer separately, and construct downstream headers from those two verified sources. Test denied requests as deliberately as successful ones.
SDK references
Hosted-agent deployment and invocation: the official invocation example requests
https://ai.azure.com/.default; it does not establish this repository's custom identity-header support.

Join the conversation! Your thoughts help the community grow.