A browser chat backed by Microsoft Agent Framework needs to remember earlier turns. When that agent is exposed through Microsoft Foundry's Responses protocol, a client can send a previous response identifier so the next answer continues from that context.
That convenience introduces an application-level authorization check. Permission to send a message does not establish permission to continue the particular conversation referenced by that message.
The change described here adds signed conversation continuation to this project's Backend for Frontend (BFF), the server-side boundary between its browser chat and Responses agent. Instead of accepting a raw provider identifier from the browser, the BFF accepts a signed continuation token tied to the authenticated user and the configured agent destination. The browser carries the token; the server validates it before continuing.
A response identifier is not proof of ownership
Microsoft's Responses API documentation shows how previous_response_id chains turns. It identifies the earlier response whose context should be used.
In a browser application, login and conversation access are separate concerns. An application JWT establishes who is making the request. A browser-supplied response identifier does not, by itself, establish that the current user is entitled to reference it.
Consider two customers using the same chat application. Each has a valid login. The application should not treat an identifier copied from one customer's conversation as sufficient authority for the other customer to continue it. Provider access controls remain important, but the application also needs to enforce its own customer boundary.
This example comes from a banking-assistant prototype extending Microsoft's public banking-assistant sample. This project added a dedicated Identity service and a Python Responses BFF as application-specific extensions. Its project overview describes those boundaries alongside its PostgreSQL pipeline, dispute/operator workflow, frontend, and Terraform choices. These extensions should not be read as features supplied by the upstream sample or required by Foundry hosting.
The BFF was already part of this project's architecture before the continuation mechanism described here. This article explains the signed-reference check added at that boundary, not the initial BFF implementation, login setup, agent deployment, or business-data authorization.
Where Agent Framework, Foundry, and the BFF fit
In this prototype, Microsoft Agent Framework defines the banking workflow: a triage agent hands requests to an Account or Transaction specialist. The workflow implementation builds that orchestration with HandoffBuilder. Those specialists decide which tools to call; they do not decide whether a browser may reuse a conversation reference.
The agent entry point uses FoundryChatClient for model access and wraps the workflow as a WorkflowAgent. A custom IsolatedResponsesHostServer, based on ResponsesHostServer from agent_framework_foundry_hosting, exposes it through /responses. Microsoft's Agent Framework hosting guide explains this Responses protocol for local testing and Foundry hosted agents.
These are separate responsibilities: Agent Framework supplies the workflow, the Responses host supplies the HTTP conversation protocol, and this project's BFF enforces the application's signed-in-user boundary. The hosting guide documents how to expose an agent; it is not the source of this custom BFF or its continuation-token contract. Foundry platform authentication does not replace the banking application's customer check. The same BFF continuation mechanism targets a local Responses host or a Foundry destination, but its token is bound to that specific destination; the offline checks below do not prove the deployed Foundry path.
Let the BFF issue the continuation
The Responses host and provider still manage conversation state. The BFF does not copy the transcript into a new ownership database. It signs a reference to a successfully completed response instead.
On the first turn, the browser sends the message and its application JWT. After successful completion, the BFF returns a continuation token. On the next turn, the browser sends that token in the application's conversation field. The BFF verifies it and constructs previous_response_id itself.
For example, suppose a customer asks for their account balance, then asks a follow-up:
Step | Browser-to-BFF request or BFF result |
|---|
First request | The customer sends “What is my account balance?” with their application JWT and no conversation token. |
Successful completion | The provider returns a completed response ID. The BFF signs that reference for this user and destination and returns the token to the browser. |
Second request | The customer sends “Which account does that balance belong to?” with their application JWT and the returned token in conversation. |
Validated continuation | The BFF validates the token, removes conversation, and supplies the decoded ID as previous_response_id to the provider. |
This is an illustrative exchange, not captured model output. The token carries permission to reference the earlier response; it does not contain the answer or account data.

Figure 1. The browser carries the token; the BFF validates identity and scope before constructing provider continuation. Raw IDs and cross-user tokens stop at the boundary. Implementation schematic, not a runtime trace.
The Responses router rejects browser requests containing previous_response_id or agent_session_id, including null values. This prevents an alternative browser field from bypassing token validation.
Here, conversation is an application-specific signed token, not a provider conversation ID. The BFF removes it before forwarding the request. Invalid tokens are rejected rather than treated as a request for a new thread.
The following excerpt shows the central transformation from the pinned router. It is partial, not a runnable application: the surrounding route has already authenticated the customer, rejected raw provider fields and checked the token's input type.
upstream_payload = dict(payload)
upstream_payload.pop("conversation", None)
if requested_conversation is not None:
upstream_payload["previous_response_id"] = _previous_response(
requested_conversation, user.sub, settings
)
_previous_response validates the signature and scope before returning the decoded response ID. Omitting the token starts a new thread; providing an invalid token stops the request before forwarding.
What the signature checks
The signing module uses HMAC-SHA256, a keyed signature that the server can verify without storing each issued token.
This signature protects the browser-to-BFF continuation reference, not the agent's connection to tools through the Model Context Protocol (MCP). OAuth can authenticate that tool connection without replacing the continuation signature: conversation access still needs its own user-and-destination check.
The signature covers:
The authenticated user's subject identifier.
The upstream mode, distinguishing local and Foundry destinations.
The configured agent endpoint.
The versioned, encoded response identifier.
Changing the response identifier invalidates the signature. Sending the token under another user's authenticated subject also fails. A token issued for a different mode or endpoint does not validate against the current destination.
The response identifier is Base64URL-encoded, not encrypted. Provider output may also expose identifiers. The protection comes from refusing to accept a raw identifier as browser authority, not from keeping identifiers secret.
Authentication remains a separate check. Before each protected request, the BFF checks the current identity with the Identity service. A revoked or stale identity cannot continue simply because it still holds a signed token. The continuation signature itself does not include the identity version, and an already admitted stream is not cancelled by a later identity change.
Wait until the response really finishes
Streaming makes issuance timing important. Visible answer text is not the same as a successfully completed response.
For a JSON response, the BFF requires a successful upstream HTTP status and a completed response with a valid identifier. It returns the token in X-Conversation-Id.
For Server-Sent Events (SSE), the continuation client observes the stream while forwarding it. It issues a final bff.continuation event only after upstream iteration ends, with a valid completion and no observed failure. A failure arriving after an earlier completion event prevents token issuance. Conflicting completion identifiers do too.
If iteration ends normally with a successful upstream HTTP status but without that confirmation, the BFF emits CONTINUATION_UNAVAILABLE with allow_retry: false. A transport exception can instead interrupt the stream without delivering that control event; non-success HTTP responses do not receive it either. None of these paths issues a new continuation token.
The BFF does not invent a response identifier or imply that resending the previous request is safe. A tool might have performed an action before the connection failed; conversation continuation cannot resolve that uncertainty.
What the tests demonstrate
A focused local selection of the continuation and Responses test modules passed 51 tests, with seven telemetry tests explicitly deselected. The run used OTEL_SDK_DISABLED=true and the existing BFF environment without dependency synchronization:
uv run --directory app\responses-bff --no-sync python -m pytest tests\test_continuation.py tests\test_responses.py -q -k 'not w3c_context_propagates_to_agent and not concurrent_bff_requests_have_independent_traces'
The tests use synthetic identities and mocked upstream responses, covering both JSON and streaming paths. The deselected cases check trace propagation and concurrent trace isolation; this result does not validate telemetry or establish full-module success.
Test group | Checked behavior |
|---|
Two-turn requests | A valid signed token becomes a server-supplied previous_response_id. |
Altered or wrong-scope tokens | Tampered, cross-user, wrong-mode and wrong-endpoint tokens are rejected. |
Browser request fields | Raw provider and session identifiers cannot bypass validation. |
Identity changes | Revoked, stale, wrong-role and unavailable identities stop forwarding. |
Stream completion | Fragmented events, missing completion, conflicting IDs and later failure are handled without issuing unsafe continuation. |
These local offline checks validate the protocol boundary, not browser behavior, real-model output, hosted identity transport, or production readiness. The source and validation snapshots are linked below; this result is not a CI run.
The trade-off: permission to continue, not a checkpoint
Stateless signing avoids a BFF conversation-ownership table. It also means there is no per-token expiry timestamp, consumption record, or individual revocation record in this implementation. A valid token can be reused within its matching scope while request authentication succeeds and the provider can resolve the response.
The token is not a transcript archive, workflow checkpoint, or exactly-once guarantee. Business services still own resource authorization and mutation safeguards. Continuing a conversation does not grant access to another customer's account or establish that a failed action can be repeated.
The transferable pattern is small: authenticate the request, validate a server-issued reference in that user's scope, and let the server construct the provider continuation field. Before adopting it, test the same boundary through the deployed browser-to-BFF path and decide whether the application also needs explicit token expiry or stateful revocation.
Implementation and references
Join the conversation! Your thoughts help the community grow.