The Verified Identity Agent Bridge
Author(s): N Selvaraj Originally published on Towards AI. The Verified Identity Agent Bridge Verify the human at the trusted edge, then carry that identity as explicit context to every downstream agent. Never collapse a user-initiated action into a shared service principal. The problem An enterprise wants its employees to reach an internal assistant from a commercial AI platform they already use every day. The assistant is a low-code agent: a Copilot Studio bot wired to an enterprise search index and a set of Power Automate flows. The integration looks simple. The host platform calls a proxy, the proxy calls the agent, the agent answers. A working prototype ships in a week. Then the question that matters arrives. An employee asks the assistant to act on something only they should see, and someone asks who, exactly, made that request. The proxy authenticated to the downstream agent with a single application credential. Every call the agent received that month carries the same actor: the proxy’s service principal. The humans who actually asked the questions are nowhere in the agent’s context or its flow run history. Personalization is impossible because the agent never learned who is asking. Authorization is impossible for the same reason. The identity was present at the front door and discarded one hop later. This is the default failure mode when a consumer-facing AI platform is bridged into an enterprise agent. The host platform verifies a human. The proxy in the middle, built for speed, authenticates downstream with client credentials because that is the path of least resistance. Identity terminates at the proxy. The claim When integrating a host AI platform with a downstream enterprise agent through a proxy, the proxy should verify the human identity using the enterprise identity provider’s delegated flow, resolve the canonical identity claims from the provider’s own source of truth, and forward that verified identity to the downstream agent as explicit per-request context. The app-only service-principal path is reserved for genuinely user-less work and logged as an exception, never used as the default for a user-initiated action. Preserving verified human identity across this bridge is the precondition for any US enterprise to adopt a third-party AI front end over internal systems without losing user-level accountability, which is what governs whether regulated organizations can use commercial AI platforms at all. There is a wrinkle that defines the pattern. The downstream agent here is a low-code SaaS agent. It cannot validate a user-audience OAuth token the way a custom API can. So identity cannot be propagated by token exchange alone. It has to be verified at the bridge and asserted downstream as trusted context. That constraint is the reason this is a distinct pattern rather than a footnote to delegated-token forwarding. The pattern: Verified Identity Agent Bridge The bridge is the one component on the path that both authenticates the human and speaks to the downstream agent. It carries the identity across the gap. Five constraints define it: Verify the human at the bridge with a delegated flow. Use the enterprise IdP’s Authorization Code flow with PKCE [1][3], not the host platform’s opaque session and not a static API key shared across users. The bridge ends up holding a token that was issued to the actual caller. Resolve identity from the IdP’s source of truth. Do not trust identity self-asserted by the host platform. Call the directory (Microsoft Graph /me) with the user's delegated token and read the canonical claims [4]: object id, user principal name, display name. The identity is what the IdP says it is. Hold no standing user credential. Tokens are request-scoped. Cache an access token only within its own lifetime and only keyed to its own user. Never share a token across users, never persist one past expiry. Assert the verified identity downstream under an explicit trust contract. The bridge forwards the resolved claims to the agent as per-request context. Because the agent cannot itself validate a user token, the channel between bridge and agent must be authenticated so the agent can trust that the asserted identity came from the bridge and not from an arbitrary caller. Gate the service-principal path. When no user token is present, the temptation is to fall back to app-only client credentials [2]. That silently re-terminates identity. Reserve it for user-less operations, gate it, and log every use as an exception. Source: Image by the author. VIAB flow: identity is verified at the bridge, resolved from the directory, and asserted downstream as per-request context over an authenticated channel. Source: Image by the author. The downstream agent cannot validate a user token, so the bridge does not forward one. It verifies the human, then vouches for them over a channel the agent already trusts. Implementation The worked example bridges ChatGPT Enterprise to a Microsoft Copilot Studio agent through a Node.js MCP proxy. ChatGPT Enterprise is the host AI platform, the Copilot Studio bot is the low-code downstream agent reached through a Power Automate flow [5], and the enterprise IdP is Microsoft Entra ID. This is the exact case VIAB is built for: ChatGPT Enterprise verifies the employee, but the Copilot Studio agent on the far side cannot validate a user-audience token, so the bridge has to verify the human and assert the identity across to it. The pattern was validated experimentally against a live integration between these two systems, with a ChatGPT Enterprise action invoking the Copilot Studio agent through the proxy. Verify the human at the bridge The delegated flow starts with PKCE so the authorization code cannot be replayed by an interceptor. The bridge builds the authorization URL, the user signs in against Entra, and the returned code is exchanged for a token issued to that user. import { randomBytes, createHash } from "crypto";export function generatePKCE() { const codeVerifier = randomBytes(32).toString("base64url"); const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url"); return { codeVerifier, codeChallenge };}export function getAuthorizationUrl(scope: string, redirectUri: string) { const { codeVerifier, codeChallenge } = generatePKCE(); const state = randomBytes(16).toString("base64url"); const params = new URLSearchParams({ client_id: process.env.CLIENT_ID!, response_type: "code", […]
