feat: Extend microsoft-agent-s2s provider with Entra Agent ID user-principal tokens (user_fic)
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 8.7k
- Forks
- 1.3k
- Avg merge
- 2d 11h
- Merged PRs (30d)
- 253
Description
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 #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 #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 #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:
- Reuse the existing two-step exchange (T1 = blueprint exchange token, T2 = agent identity exchange token). Both are already minted today by PR #1424 — for app-only tokens, the broker stops after T2 and exchanges T2 for the Graph app-token.
- Add a third broker method, mint_user_fic_token(scope), that uses both T1 and T2 to call the user_fic grant per the Entra Agent ID protocol (https://learn.microsoft.com/en-us/entra/agent-id/identity-platform/agent-user-oauth-flow):
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:
- Existing path (PR #1424): GET http://10.200.0.1:3130/v1/microsoft-agent-s2s/token/?audience=api://resource returns an app-only token.
- New path: GET http://10.200.0.1:3130/v1/microsoft-agent-s2s/user-token/?scope=Mail.Read returns a user-principal token.
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
- 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.
- 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.
- 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.
- Keep PR #1424 as-shipped, file this as a strict "later" issue. Acceptable but loses the opportunity to do a small naming cleanup before PR #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 #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 #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 #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
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reviewing PR #1424 and the existing Microsoft S2S flow, then read crates/openshell-server/src/provider_auth/microsoft_s2s.rs, crates/openshell-sandbox/src/provider_tokens.rs, and the dispatch in crates/openshell-server/src/grpc/policy.rs. Verify the documented user_fic exchange and how the proposed request distinction fits the existing resolver. Done means one provider supports both token paths with the stated configuration, caching, and environment-variable behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- api, authentication, backend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100