Skip to content

feat: Extend microsoft-agent-s2s provider with Entra Agent ID user-principal tokens (user_fic) #1667

@yucliang-nv

Description

@yucliang-nv

Problem Statement

PR #1424 introduces the microsoft-agent-s2s provider for Microsoft Entra Agent ID app-only tokens (the client_credentials grant). This is one of the two token types Entra Agent ID issues for a given agent. The other is the user-principal token for the agent's bound user account, issued via the user_fic grant.

Both token types are emitted by the same Entra Agent ID configuration:

  • Same tenant.
  • Same agent identity blueprint.
  • Same agent identity (child of blueprint).
  • Same federated credential trust on the blueprint.
  • The agent user account is provisioned 1:1 with the agent identity, using Microsoft Graph subtype microsoft.graph.agentUser linked via identityParentId.

The two token types are used by the agent for different classes of Microsoft Graph endpoints:

  • App-only tokens (already in PR Feat microsoft provider v2 #1424, via client_credentials grant + FIC): work for app-permissioned Graph endpoints (whatever app permissions the agent identity holds). They cannot access /me/* endpoints because they carry no user identity.
  • User-principal tokens (proposed here, via user_fic grant + FIC + agent-identity exchange chain): work for the agent's own /me/* endpoints — the agent's mailbox (read and send), Teams chat as the agent's first-class identity, the agent's personal SharePoint and OneDrive, calendar, and any other endpoint that requires a user-context token.

Because both grants are properties of the same agent in Entra, splitting them across two OpenShell providers forces operators to duplicate Entra configuration in two provider records (same tenant, blueprint, agent identity in each) and forces the sandbox agent to know about two separate resolver URLs. Treating both grants as facets of a single microsoft-agent-s2s provider keeps the OpenShell representation aligned with the Entra reality: one agent configuration becomes one provider record with two token-grant endpoints.

Today, teams that need user-principal tokens for agents typically build out-of-tree sidecars: a separate container that signs in as a per-agent regular Entra user account (using MSAL device-code flow against a vaulted password), refreshes tokens, and exposes them to the sandboxed agent over a local HTTP shim. This works but carries a long-lived password as the agent's root credential, has no attestation tying the runtime to the credential, produces audit logs that look indistinguishable from human sign-in, and reinvents the same plumbing per team. Entra Agent ID's user_fic grant is the native, attestation-based alternative.

Proposed Design

Extend the microsoft-agent-s2s provider (PR #1424) to also produce user-principal tokens via the user_fic grant. Single provider record, two resolver paths in the sandbox-local HTTP shim.

Naming. The current provider id microsoft-agent-s2s ("S2S" = service-to-service) becomes a misnomer once it also handles user-context tokens. Two options worth discussing in this issue thread:

  • Rename the provider to microsoft-agent-id or microsoft-agent. Since PR Feat microsoft provider v2 #1424 hasn't merged, this can be coordinated with the PR author and the rename has zero migration cost.
  • Keep microsoft-agent-s2s and document the historical name in the description. Less disruption to PR Feat microsoft provider v2 #1424; downside is the name doesn't describe the full scope.

This issue doesn't pick one — flagging for discussion.

Config additions. The existing provider config (tenant, blueprint client ID, agent identity client ID, federated credential trust) is reused unchanged. Two new fields support the user-principal grant:

config:

... existing fields from PR #1424 ...

  • name: ENTRA_AGENT_USER_UPN
    description: |
    The UPN of the agent's user account (Microsoft Graph
    microsoft.graph.agentUser linked to the agent identity via
    identityParentId). Required if the agent will request user_fic tokens.
    required: false
  • name: ENTRA_USER_FIC_DEFAULT_SCOPES
    description: |
    Comma-separated Graph scopes embedded in user-principal tokens by default.
    Example: "Mail.Read,Mail.Send,ChatMessage.Send,Sites.Read.All"
    required: false

ENTRA_AGENT_USER_UPN is the marker that the provider is configured for user-principal issuance. If absent, only the existing S2S endpoint is active; if present, both endpoints are active.

Broker (gateway-side). Extend the existing broker in crates/openshell-server/src/provider_auth/microsoft_s2s.rs:

POST https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token
client_id={agent_identity}
grant_type=user_fic
client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
client_assertion={T1}
user_federated_identity_credential={T2}
username={agent_user_upn}
scope={requested_graph_scope}

  • Reuse the existing T1/T2 cache. Add a third cache for user-principal tokens keyed by (tenant, agent_identity, agent_user_upn, scope) with the same refresh skew (~5 min) the S2S broker uses.

Sandbox-side resolver. Extend the resolver in crates/openshell-sandbox/src/provider_tokens.rs. Two paths under the same provider's local HTTP endpoint:

The agent gets a second env var alongside the existing ones: OPENSHELL_MICROSOFT_AGENT_USER_TOKEN_URL pointing at the new path. The user-token URL is only injected when ENTRA_AGENT_USER_UPN is set on the provider config.

Broker dispatch. The handle_mint_sandbox_provider_token handler in crates/openshell-server/src/grpc/policy.rs already dispatches by provider.type to MicrosoftS2sBroker. The broker internally chooses between app-only and user-principal exchange based on the request path or a dedicated request field — minor proto addition to MintSandboxProviderTokenRequest to distinguish the two:

message MintSandboxProviderTokenRequest {
string sandbox_id = 1;
string provider_name = 2;
string audience = 3;
// NEW: which token type to mint. Default = app_only for backward compatibility.
enum TokenKind {
TOKEN_KIND_UNSPECIFIED = 0;
APP_ONLY = 1; // client_credentials grant
USER_PRINCIPAL = 2; // user_fic grant
}
TokenKind kind = 4;
}

Alternatives Considered

  1. Separate sibling provider microsoft-agent-user-fic. Workable but forces operators to duplicate Entra config (tenant, blueprint, agent identity, FIC trust) in two provider records that always describe the same agent. The two token types are properties of one Entra agent configuration, not two independent providers.
  2. Out-of-tree Python sidecar with vaulted password and MSAL device-code flow. Long-lived credentials, no attestation, per-team duplicate implementations, configuration sprawl outside OpenShell.
  3. Different resolver path layout (e.g., one path with a ?kind=user|app query). Functionally equivalent to the two-path proposal. Two paths give the agent clearer-to-read URLs and let the resolver route at the URL-prefix level without parsing query strings. Either works.
  4. Keep PR Feat microsoft provider v2 #1424 as-shipped, file this as a strict "later" issue. Acceptable but loses the opportunity to do a small naming cleanup before PR Feat microsoft provider v2 #1424 lands. Renaming after merge is harder.

Agent Investigation

  • Protocol works as documented. Successfully exercised the three-step exchange (blueprint → agent identity → user_fic) against the v2.0 token endpoint and obtained a Microsoft Graph access token with idtyp=user and the agent user account's oid as the token subject. Matches the spec at https://learn.microsoft.com/en-us/entra/agent-id/identity-platform/agent-user-oauth-flow.
  • The blueprint federated credential is shared between the two grants. A single FIC trust on the blueprint app — verified at applications/{blueprint-id}/federatedIdentityCredentials via Microsoft Graph — authenticates both the client_credentials path (PR Feat microsoft provider v2 #1424's existing flow) and the user_fic path proposed here. No additional Entra-side setup beyond the agent user account creation, which is already part of Entra Agent ID's standard onboarding.
  • PR Feat microsoft provider v2 #1424's existing broker structure naturally accommodates the extension. The MicrosoftS2sBroker already implements the T1/T2 token-chain caching that user_fic reuses. Adding mint_user_fic_token is a third method on the same struct (≈80 lines including the cache, refresh, and unverified JWT decode logic — modeled on the existing mint_runtime_agent_token).
  • Sandbox-side resolver code in PR Feat microsoft provider v2 #1424 at crates/openshell-sandbox/src/provider_tokens.rs is already structured around per-provider HTTP paths. Adding a second path under the same provider is incremental.

Checklist

  • I've reviewed existing issues and the architecture docs
  • This is a design proposal, not a "please build this" request

Metadata

Metadata

Assignees

No one assigned

    Labels

    state:triage-neededOpened without agent diagnostics and needs triage

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions