Entra ID Auth Sidecar Provider — Python Agents SDK
Summary
Add a new authentication provider package (microsoft-agents-authentication-entra-sidecar) to the Python Microsoft 365 Agents SDK that integrates with the Microsoft Entra ID Agent Container (sidecar). This provider implements the existing AccessTokenProviderBase protocol by delegating token acquisition to the sidecar's HTTP API rather than directly using the MSAL library.
Initial Scope: Acquire the base Blueprint Agentic Identity token from the sidecar container, then leverage the existing SDK resolution path for Agent Instance and Agent User tokens.
Motivation
The Entra ID Agent Container provides a language-agnostic, credential-free authentication sidecar that:
- Eliminates credential handling in agent code — The agent never touches secrets, certificates, or keys.
- Simplifies Python deployments — No MSAL dependency, no certificate file management, no
asyncio.to_thread() wrapping of synchronous MSAL calls.
- Provides production-grade security — The sidecar supports Workload Identity, Managed Identity, Key Vault certificates, and federated credentials out of the box.
- Enables consistent local development — Docker Compose with the sidecar provides the same authentication experience locally as in production.
Scope
In Scope (Phase 1)
| Capability |
Description |
| Blueprint Identity Acquisition |
Acquire the base Blueprint app token via GET /AuthorizationHeaderUnauthenticated/{name} |
| Agent Instance Resolution |
Pass the Blueprint token through the existing SDK path (get_agentic_instance_token) |
| Agent User Resolution |
Pass the Blueprint token through the existing SDK path (get_agentic_user_token) |
AccessTokenProviderBase implementation |
Full implementation of get_access_token and get_agentic_application_token delegating to the sidecar |
Connections protocol implementation |
SidecarConnectionManager implementing the connection registry |
| Configuration model |
New SidecarAuthConfiguration leveraging AgentAuthConfiguration patterns |
| Health check |
Validate sidecar availability via GET /healthz at startup |
Out of Scope (Future Phases)
| Capability |
Rationale |
| OBO token exchange via sidecar |
Requires inbound user token; Phase 2 |
Downstream API proxy (/DownstreamApi/* endpoints) |
SDK does not proxy API calls through auth providers |
Token validation (/Validate endpoint) |
SDK handles inbound token validation separately |
acquire_token_on_behalf_of via sidecar |
Deferred to Phase 2 |
Technical Design
Architecture
┌─────────────────────────────────────────────────────────┐
│ Agent Application (Python) │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Agents SDK │ │
│ │ │ │
│ │ AccessTokenProviderBase ◄── SidecarAuth │ │
│ │ │ │ │
│ │ │ get_agentic_application_token() │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ SidecarTokenClient │ ─── HTTP ───┐ │ │
│ │ └─────────────────────────┘ │ │ │
│ └───────────────────────────────────────────┼───────┘ │
│ │ │
└──────────────────────────────────────────────┼───────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ Entra ID Agent Container (Sidecar) :5000 │
│ │
│ GET /AuthorizationHeaderUnauthenticated/{name} │
│ ?AgentIdentity={agent-client-id} │
│ │
│ Response: { "authorizationHeader": "Bearer <token>" } │
└──────────────────────────────────────────────────────────┘
│
▼
Microsoft Entra ID (login.microsoftonline.com)
Base Protocol (existing — from microsoft-agents-hosting-core)
class AccessTokenProviderBase(Protocol):
@abstractmethod
async def get_access_token(
self, resource_url: str, scopes: list[str], force_refresh: bool = False
) -> str:
pass
async def acquire_token_on_behalf_of(
self, scopes: list[str], user_assertion: str
) -> str:
raise NotImplementedError()
async def get_agentic_application_token(
self, tenant_id: str, agent_app_instance_id: str
) -> Optional[str]:
raise NotImplementedError()
async def get_agentic_instance_token(
self, tenant_id: str, agent_app_instance_id: str
) -> tuple[str, str]:
raise NotImplementedError()
async def get_agentic_user_token(
self,
tenant_id: str,
agent_app_instance_id: str,
agentic_user_id: str,
scopes: list[str],
) -> Optional[str]:
raise NotImplementedError()
SidecarAuth — Provider Implementation
import os
import logging
from typing import Optional
import httpx
from microsoft_agents.hosting.core import (
AccessTokenProviderBase,
AgentAuthConfiguration,
)
logger = logging.getLogger(__name__)
class SidecarAuth(AccessTokenProviderBase):
"""
Authentication provider that delegates token acquisition to the
Microsoft Entra ID Agent Container (sidecar).
"""
def __init__(self, configuration: AgentAuthConfiguration):
self._configuration = configuration
# Resolution order: SIDECAR_URL env var > sidecar_base_url config > default
self._sidecar_base_url = (
os.environ.get("SIDECAR_URL")
or getattr(configuration, "sidecar_base_url", None)
or "http://localhost:5000"
)
self._http_client = httpx.AsyncClient(
base_url=self._sidecar_base_url,
timeout=httpx.Timeout(30.0),
)
async def get_access_token(
self, resource_url: str, scopes: list[str], force_refresh: bool = False
) -> str:
"""
Acquire an app-only access token from the sidecar.
Called by SDK internals for service-to-service auth.
"""
response = await self._http_client.get(
"/AuthorizationHeaderUnauthenticated/default"
)
response.raise_for_status()
return self._parse_token(response.json())
async def get_agentic_application_token(
self, tenant_id: str, agent_app_instance_id: str
) -> Optional[str]:
"""
Acquire the Blueprint agentic identity token from the sidecar.
The agent_app_instance_id is passed as the AgentIdentity query parameter.
This value comes from the inbound ActivityProtocol request.
"""
response = await self._http_client.get(
"/AuthorizationHeaderUnauthenticated/default",
params={"AgentIdentity": agent_app_instance_id},
)
response.raise_for_status()
return self._parse_token(response.json())
async def get_agentic_instance_token(
self, tenant_id: str, agent_app_instance_id: str
) -> tuple[str, str]:
"""
Acquire the Agent Instance token.
Uses the Blueprint token from get_agentic_application_token,
then follows the existing SDK path to derive the instance token.
The agent_app_instance_id comes from the inbound ActivityProtocol request.
"""
# Step 1: Get Blueprint token from sidecar
agent_token = await self.get_agentic_application_token(
tenant_id, agent_app_instance_id
)
if not agent_token:
raise RuntimeError(
f"Failed to acquire agentic application token for instance {agent_app_instance_id}"
)
# Step 2: Use existing SDK path — exchange Blueprint token for Instance token
# The agent_token is used as client_assertion in a new CCA
# with client_id = agent_app_instance_id
instance_app = ConfidentialClientApplication(
client_id=agent_app_instance_id,
authority=f"https://login.microsoftonline.com/{tenant_id}",
client_credential={"client_assertion": agent_token},
)
result = instance_app.acquire_token_for_client(
scopes=["api://AzureAdTokenExchange/.default"]
)
if "access_token" not in result:
raise RuntimeError(
f"Failed to acquire agentic instance token: {result.get('error_description', 'unknown error')}"
)
return (result["access_token"], agent_token)
async def get_agentic_user_token(
self,
tenant_id: str,
agent_app_instance_id: str,
agentic_user_id: str,
scopes: list[str],
) -> Optional[str]:
"""
Acquire a user-scoped token using the agentic identity chain.
The agent_app_instance_id and agentic_user_id come from the
inbound ActivityProtocol request.
Uses the existing SDK path:
1. Get instance_token + agent_token via get_agentic_instance_token
2. Exchange for user token via user_fic grant
"""
instance_token, agent_token = await self.get_agentic_instance_token(
tenant_id, agent_app_instance_id
)
# Use agent_token as client_assertion, exchange for user token
user_app = ConfidentialClientApplication(
client_id=agent_app_instance_id,
authority=f"https://login.microsoftonline.com/{tenant_id}",
client_credential={"client_assertion": agent_token},
)
result = user_app.acquire_token_for_client(
scopes=scopes,
data={
"user_id": agentic_user_id,
"user_federated_identity_credential": instance_token,
"grant_type": "user_fic",
},
)
if "access_token" not in result:
raise RuntimeError(
f"Failed to acquire agentic user token: {result.get('error_description', 'unknown error')}"
)
return result["access_token"]
async def is_healthy(self) -> bool:
"""Check sidecar availability via /healthz endpoint."""
try:
response = await self._http_client.get("/healthz")
return response.status_code == 200
except httpx.HTTPError:
return False
async def close(self):
"""Close the underlying HTTP client."""
await self._http_client.aclose()
@staticmethod
def _parse_token(response_body: dict) -> str:
"""
Extract raw access token from sidecar response.
Response format: { "authorizationHeader": "Bearer eyJ..." }
"""
auth_header = response_body.get("authorizationHeader", "")
if auth_header.startswith("Bearer "):
return auth_header[7:]
return auth_header
Parameter Resolution from ActivityProtocol
The AgentIdentity (agent instance ID) and AgentUserId (agent object ID) values are not stored in configuration — they are extracted from the inbound ActivityProtocol request at runtime:
| Parameter |
Source in ActivityProtocol Request |
Passed To |
agent_app_instance_id |
The Agent Instance ID from the activity request |
get_agentic_application_token, get_agentic_instance_token, get_agentic_user_token — mapped to the AgentIdentity query parameter on the sidecar |
agentic_user_id |
The Agent User object ID from the activity request |
get_agentic_user_token — mapped to the AgentUserId query parameter on the sidecar |
This means the SDK resolves identity per-request based on the incoming activity, enabling a single agent deployment to serve multiple agent identities and users without reconfiguration.
Token Flow — Blueprint Identity (Phase 1 Focus)
Agent SDK (Python) Sidecar (:5000) Entra ID
│ │ │
│ get_agentic_application_token() │ │
│──────────────────────────────────►│ │
│ GET /AuthorizationHeaderUnauthenticated/default │
│ ?AgentIdentity={agent_app_instance_id} │
│ │ │
│ │ Client Credentials (Blueprint) │
│ │───────────────────────────────►│
│ │◄─────────── T1 (Blueprint) ────│
│ │ │
│ │ FIC Exchange (AgentIdentity) │
│ │───────────────────────────────►│
│ │◄─────────── TR (Agent App) ────│
│ │ │
│◄─── { "authorizationHeader": │ │
│ "Bearer TR" } │ │
│ │ │
│ _parse_token() → strip "Bearer " │ │
│ return TR │ │
│ │ │
│ ─── Existing SDK path ─── │ │
│ get_agentic_instance_token() │ │
│ uses TR as client_assertion │ │
│ in ConfidentialClientApplication │ │
│ to acquire Agent Instance token │ │
│ │ │
│ get_agentic_user_token() │ │
│ uses Instance token + user_fic │ │
│ grant to acquire user token │ │
Key Insight: The sidecar provides the Blueprint application token (the first step in the agentic identity chain). The existing SDK logic for get_agentic_instance_token and get_agentic_user_token already knows how to use a Blueprint token to derive Instance and User tokens — this provider just changes how the initial Blueprint token is acquired.
Connection Manager — Refactoring to a Generic Provider-Agnostic Design
Rather than creating a sidecar-specific connection manager, the existing MsalConnectionManager should be refactored into a generic ConnectionManager that accepts any AccessTokenProviderBase implementation via a factory pattern. This enables support for additional auth providers in the future without duplicating the connection routing logic.
Rationale: The current MsalConnectionManager contains ~140 lines of connection routing logic (audience matching, service URL regex dispatch, default connection resolution) that is entirely provider-agnostic. The only MSAL-specific line is the instantiation of MsalAuth(config). Refactoring this into a generic base avoids duplicating routing logic for every new auth provider (Sidecar, future providers, etc.).
Proposed Generic Connection Manager
import re
from typing import Optional, Dict, List, Callable, Type
from microsoft_agents.hosting.core import (
Connections,
AccessTokenProviderBase,
AgentAuthConfiguration,
ClaimsIdentity,
)
class ConnectionManager(Connections):
"""
Generic connection manager that dispatches to any AccessTokenProviderBase
implementation. The provider_factory parameter determines which auth provider
is instantiated for each connection configuration.
This design supports adding new auth providers (Sidecar, MSAL, future providers)
without duplicating connection routing logic.
"""
def __init__(
self,
provider_factory: Callable[[AgentAuthConfiguration], AccessTokenProviderBase],
connections_configurations: Optional[Dict[str, AgentAuthConfiguration]] = None,
connections_map: Optional[List[Dict[str, str]]] = None,
**kwargs,
):
self._provider_factory = provider_factory
self._connections: Dict[str, AccessTokenProviderBase] = {}
self._connections_map = connections_map or kwargs.get("CONNECTIONSMAP", {})
self._config_map: Dict[str, AgentAuthConfiguration] = {}
if connections_configurations:
for name, config in connections_configurations.items():
self._connections[name] = provider_factory(config)
self._config_map[name] = config
else:
raw_configurations: Dict[str, Dict] = kwargs.get("CONNECTIONS", {})
for name, settings in raw_configurations.items():
parsed_config = AgentAuthConfiguration(
**settings.get("SETTINGS", {})
)
self._connections[name] = provider_factory(parsed_config)
self._config_map[name] = parsed_config
# JWT-patch: share connections across configs for cross-connection validation
for config in self._config_map.values():
config._connections = self._config_map
if "SERVICE_CONNECTION" not in self._connections:
raise ValueError("No SERVICE_CONNECTION configuration provided.")
def get_connection(self, connection_name: Optional[str]) -> AccessTokenProviderBase:
connection_name = connection_name or "SERVICE_CONNECTION"
connection = self._connections.get(connection_name)
if not connection:
raise ValueError(f"No connection found for '{connection_name}'.")
return connection
def get_default_connection(self) -> AccessTokenProviderBase:
return self.get_connection("SERVICE_CONNECTION")
def get_token_provider(
self, claims_identity: ClaimsIdentity, service_url: str
) -> AccessTokenProviderBase:
if not self._connections_map:
return self.get_default_connection()
aud = (claims_identity.get_app_id() or "").lower()
for item in self._connections_map:
item_aud = item.get("AUDIENCE", "").lower()
if item_aud and item_aud != aud:
continue
item_svc = item.get("SERVICEURL", "*")
if item_svc == "*" or not item_svc:
return self.get_connection(item.get("CONNECTION"))
if re.match(item_svc, service_url, re.IGNORECASE):
return self.get_connection(item.get("CONNECTION"))
return self.get_default_connection()
def get_default_connection_configuration(self) -> AgentAuthConfiguration:
config = self._config_map.get("SERVICE_CONNECTION")
if not config:
raise ValueError("No SERVICE_CONNECTION configuration found.")
return config
Usage with Sidecar Provider
from microsoft_agents.hosting.core import ConnectionManager
from microsoft_agents.authentication.entra_sidecar import SidecarAuth
# The provider_factory is simply the SidecarAuth constructor
connection_manager = ConnectionManager(
provider_factory=SidecarAuth,
CONNECTIONS=agents_sdk_config["CONNECTIONS"],
CONNECTIONSMAP=agents_sdk_config.get("CONNECTIONSMAP", []),
)
Backward Compatibility with MSAL
The existing MsalConnectionManager becomes a thin convenience wrapper:
from microsoft_agents.authentication.msal import MsalAuth
class MsalConnectionManager(ConnectionManager):
"""Convenience subclass that defaults to MsalAuth as the provider factory."""
def __init__(self, connections_configurations=None, connections_map=None, **kwargs):
super().__init__(
provider_factory=MsalAuth,
connections_configurations=connections_configurations,
connections_map=connections_map,
**kwargs,
)
Future Extensibility
Adding a new auth provider requires only:
- Implementing a class that satisfies
AccessTokenProviderBase
- Passing it as the
provider_factory to ConnectionManager
No new connection manager class is needed.
Configuration
The provider supports an optional sidecar_base_url configuration field. If the SIDECAR_URL environment variable is also present, the environment variable takes precedence.
URL Resolution Precedence:
SIDECAR_URL environment variable (highest priority)
sidecar_base_url from configuration
- Default:
http://localhost:5000
Environment variables:
| Variable |
Required |
Default |
Description |
SIDECAR_URL |
No |
http://localhost:5000 |
Sidecar HTTP endpoint (takes precedence over config setting) |
AgentAuthConfiguration fields used:
| Field |
Required |
Description |
CLIENT_ID |
Yes |
Blueprint app registration client ID |
SCOPES |
Yes |
OAuth scopes to request (e.g., ["api://.../.default"]) |
AUTH_TYPE |
No |
"EntraAuthSideCar" (new AuthTypes enum value). Defaults to "EntraAuthSideCar" when using SidecarAuth directly. |
SIDECAR_BASE_URL |
No |
Optional sidecar HTTP endpoint. Overridden by SIDECAR_URL env var if both are set. |
Note: No secrets, certificates, or tenant IDs are required in the agent's configuration. All credential management is handled by the sidecar container.
Environment variable style configuration (double-underscore pattern):
CONNECTIONS__SERVICE_CONNECTION__AUTHTYPE=EntraAuthSideCar
CONNECTIONS__SERVICE_CONNECTION__CLIENTID=<blueprint-app-id>
CONNECTIONS__SERVICE_CONNECTION__SCOPES__0=api://<blueprint-app-id>/.default
CONNECTIONS__SERVICE_CONNECTION__SIDECAR_BASE_URL=http://my-sidecar:5000 # optional config fallback
CONNECTIONS_MAP__0__SERVICEURL=*
CONNECTIONS_MAP__0__CONNECTION=SERVICE_CONNECTION
SIDECAR_URL=http://localhost:5000 # takes precedence over SIDECAR_BASE_URL if set
New AuthTypes Enum Value
class AuthTypes(str, Enum):
certificate = "certificate"
certificate_subject_name = "CertificateSubjectName"
client_secret = "ClientSecret"
user_managed_identity = "UserManagedIdentity"
system_managed_identity = "SystemManagedIdentity"
federated_credentials = "FederatedCredentials"
entra_auth_sidecar = "EntraAuthSideCar" # NEW — Entra ID Agent Container
Package Structure
Sidecar auth provider package (new):
libraries/microsoft-agents-authentication-entra-sidecar/
├── pyproject.toml
├── setup.py
├── microsoft_agents/
│ └── authentication/
│ └── entra_sidecar/
│ ├── __init__.py
│ ├── sidecar_auth.py
│ ├── sidecar_token_client.py
│ └── errors/
│ ├── __init__.py
│ └── error_resources.py
Generic ConnectionManager addition (in microsoft-agents-hosting-core):
libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/
├── connection_manager.py (NEW — generic provider-agnostic connection manager)
├── connections.py (existing — Connections protocol)
└── ...
__init__.py exports (sidecar package):
from .sidecar_auth import SidecarAuth
__all__ = ["SidecarAuth"]
Note: No SidecarConnectionManager is needed. Use the generic ConnectionManager from microsoft-agents-hosting-core with provider_factory=SidecarAuth.
pyproject.toml:
[project]
name = "microsoft-agents-authentication-entra-sidecar"
requires-python = ">=3.10"
[project.dependencies]
microsoft-agents-hosting-core = "==<version>"
httpx = ">=0.27.0"
setup.py:
from setuptools import setup, find_namespace_packages
setup(
name="microsoft-agents-authentication-entra-sidecar",
packages=find_namespace_packages(include=["microsoft_agents.*"]),
install_requires=[
f"microsoft-agents-hosting-core=={package_version}",
"httpx>=0.27.0",
],
python_requires=">=3.10",
)
Error Handling
class SidecarAuthError(Exception):
"""Base exception for sidecar authentication errors."""
pass
class SidecarUnavailableError(SidecarAuthError):
"""Raised when the sidecar is unreachable."""
pass
class SidecarConfigurationError(SidecarAuthError):
"""Raised when the sidecar returns 404 (misconfigured resource name)."""
pass
| Sidecar Response |
SDK Behavior |
200 OK + valid JSON |
Parse authorizationHeader, strip "Bearer " prefix, return token |
401 Unauthorized |
Raise SidecarAuthError — sidecar credentials misconfigured |
404 Not Found |
Raise SidecarConfigurationError — resource not configured on sidecar |
400 Bad Request |
Raise ValueError with sidecar error body (e.g., missing AgentIdentity) |
500 Internal Server Error |
Retry per policy; raise SidecarAuthError after exhausting retries |
| Connection refused / timeout |
Retry per policy; raise SidecarUnavailableError |
| Non-JSON response |
Raise SidecarAuthError with raw response for diagnostics |
Retry and Resilience
import httpx
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
RETRY_DECORATOR = retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=10),
retry=retry_if_exception_type((httpx.ConnectError, httpx.TimeoutException)),
)
Alternatively, use httpx transport-level retries or keep retry logic minimal for Phase 1 and rely on the sidecar's own resilience.
Token Caching Strategy
| Concern |
Strategy |
| Sidecar-side caching |
The sidecar itself caches tokens internally; repeated calls may return cached tokens |
| SDK-side caching |
Use the existing SDK token expiration management process (same as MSAL provider) |
force_refresh support |
Bypass SDK cache; the sidecar will still use its internal cache unless expired |
Security Considerations
-
Network isolation — The sidecar MUST only be accessible from the agent container (same pod in K8s, same Docker bridge network). The SDK MUST validate that the resolved Entra Auth SideCar Base URL points to a loopback address (e.g., localhost, 127.0.0.1, [::1]) or a private network address (RFC 1918 / RFC 4193). If the resolved URL is a non-loopback, non-private address, the provider MUST raise an error and refuse to issue requests. Exception: This validation MUST be skipped if the sidecar_base_url is explicitly set in the configuration section of the EntraAuthSideCar entry — an explicit configuration value signals intentional operator override (e.g., for sidecar deployed as a separate container with a routable address within a private network).
-
No credentials in agent config — Unlike the MSAL provider, this provider does NOT require secrets, certificates, or credential configuration.
-
Token in transit — Communication between SDK and sidecar is over HTTP (not HTTPS) within the pod boundary. This is acceptable because traffic never leaves the pod/node network namespace.
-
Response validation — Validate that authorizationHeader is non-empty and starts with Bearer before stripping the prefix.
Usage Example
from microsoft_agents.authentication.entra_sidecar import (
SidecarAuth,
SidecarConnectionManager,
)
from microsoft_agents.hosting.core import AgentAuthConfiguration
# Configuration — no secrets needed
config = AgentAuthConfiguration(
auth_type="EntraAuthSideCar",
client_id="<blueprint-app-id>",
)
# Create connection manager (SDK wiring)
manager = SidecarConnectionManager(
connections_configurations={
"SERVICE_CONNECTION": config,
}
)
# Or via dict-based config (YAML-loaded)
manager = SidecarConnectionManager(
CONNECTIONS={
"SERVICE_CONNECTION": {
"SETTINGS": {
"AUTHTYPE": "EntraAuthSideCar",
"CLIENTID": "<blueprint-app-id>",
}
}
}
)
# The SDK automatically calls get_agentic_application_token
# with agent_app_instance_id from the inbound ActivityProtocol request
Testing Strategy
| Test Type |
Description |
| Unit tests |
Mock httpx.AsyncClient to simulate sidecar responses (200, 401, 404, 500, timeout) |
| Integration tests |
Docker Compose with actual sidecar container + test Entra tenant |
| Health check tests |
Verify startup behavior when sidecar is unavailable |
| E2E tests |
Full agent scenario: SDK → Sidecar → Entra ID → downstream API |
Open Questions
| # |
Question |
Impact |
| 1 |
Should the SDK-side cache read the JWT exp claim, or rely solely on a configured TTL? Resolved: Use the existing SDK token expiration management process (same as MSAL provider). |
— |
| 2 |
Should get_agentic_instance_token and get_agentic_user_token also delegate to the sidecar (future), or always use the existing SDK path after getting the Blueprint token? Resolved: Use the existing SDK built-in system. Can be revisited in a future phase. |
— |
| 3 |
What is the startup behavior if /healthz fails? Fail-fast (raise) or deferred-failure (fail on first token request)? |
Developer experience |
| 4 |
Should httpx or aiohttp be the HTTP client? (httpx recommended for async-first design and consistent API) |
Dependency choice |
| 5 |
Does the sidecar return token expiry metadata or should the SDK always parse the JWT? |
Token caching implementation |
References
Entra ID Auth Sidecar Provider — Python Agents SDK
Summary
Add a new authentication provider package (
microsoft-agents-authentication-entra-sidecar) to the Python Microsoft 365 Agents SDK that integrates with the Microsoft Entra ID Agent Container (sidecar). This provider implements the existingAccessTokenProviderBaseprotocol by delegating token acquisition to the sidecar's HTTP API rather than directly using the MSAL library.Initial Scope: Acquire the base Blueprint Agentic Identity token from the sidecar container, then leverage the existing SDK resolution path for Agent Instance and Agent User tokens.
Motivation
The Entra ID Agent Container provides a language-agnostic, credential-free authentication sidecar that:
asyncio.to_thread()wrapping of synchronous MSAL calls.Scope
In Scope (Phase 1)
GET /AuthorizationHeaderUnauthenticated/{name}get_agentic_instance_token)get_agentic_user_token)AccessTokenProviderBaseimplementationget_access_tokenandget_agentic_application_tokendelegating to the sidecarConnectionsprotocol implementationSidecarConnectionManagerimplementing the connection registrySidecarAuthConfigurationleveragingAgentAuthConfigurationpatternsGET /healthzat startupOut of Scope (Future Phases)
/DownstreamApi/*endpoints)/Validateendpoint)acquire_token_on_behalf_ofvia sidecarTechnical Design
Architecture
Base Protocol (existing — from
microsoft-agents-hosting-core)SidecarAuth— Provider ImplementationParameter Resolution from ActivityProtocol
The
AgentIdentity(agent instance ID) andAgentUserId(agent object ID) values are not stored in configuration — they are extracted from the inbound ActivityProtocol request at runtime:agent_app_instance_idget_agentic_application_token,get_agentic_instance_token,get_agentic_user_token— mapped to theAgentIdentityquery parameter on the sidecaragentic_user_idget_agentic_user_token— mapped to theAgentUserIdquery parameter on the sidecarThis means the SDK resolves identity per-request based on the incoming activity, enabling a single agent deployment to serve multiple agent identities and users without reconfiguration.
Token Flow — Blueprint Identity (Phase 1 Focus)
Key Insight: The sidecar provides the Blueprint application token (the first step in the agentic identity chain). The existing SDK logic for
get_agentic_instance_tokenandget_agentic_user_tokenalready knows how to use a Blueprint token to derive Instance and User tokens — this provider just changes how the initial Blueprint token is acquired.Connection Manager — Refactoring to a Generic Provider-Agnostic Design
Rather than creating a sidecar-specific connection manager, the existing
MsalConnectionManagershould be refactored into a genericConnectionManagerthat accepts anyAccessTokenProviderBaseimplementation via a factory pattern. This enables support for additional auth providers in the future without duplicating the connection routing logic.Rationale: The current
MsalConnectionManagercontains ~140 lines of connection routing logic (audience matching, service URL regex dispatch, default connection resolution) that is entirely provider-agnostic. The only MSAL-specific line is the instantiation ofMsalAuth(config). Refactoring this into a generic base avoids duplicating routing logic for every new auth provider (Sidecar, future providers, etc.).Proposed Generic Connection Manager
Usage with Sidecar Provider
Backward Compatibility with MSAL
The existing
MsalConnectionManagerbecomes a thin convenience wrapper:Future Extensibility
Adding a new auth provider requires only:
AccessTokenProviderBaseprovider_factorytoConnectionManagerNo new connection manager class is needed.
Configuration
The provider supports an optional
sidecar_base_urlconfiguration field. If theSIDECAR_URLenvironment variable is also present, the environment variable takes precedence.URL Resolution Precedence:
SIDECAR_URLenvironment variable (highest priority)sidecar_base_urlfrom configurationhttp://localhost:5000Environment variables:
SIDECAR_URLhttp://localhost:5000AgentAuthConfigurationfields used:CLIENT_IDSCOPES["api://.../.default"])AUTH_TYPE"EntraAuthSideCar"(newAuthTypesenum value). Defaults to"EntraAuthSideCar"when usingSidecarAuthdirectly.SIDECAR_BASE_URLSIDECAR_URLenv var if both are set.Note: No secrets, certificates, or tenant IDs are required in the agent's configuration. All credential management is handled by the sidecar container.
Environment variable style configuration (double-underscore pattern):
New
AuthTypesEnum ValuePackage Structure
Sidecar auth provider package (new):
Generic
ConnectionManageraddition (inmicrosoft-agents-hosting-core):__init__.pyexports (sidecar package):pyproject.toml:setup.py:Error Handling
200 OK+ valid JSONauthorizationHeader, strip"Bearer "prefix, return token401 UnauthorizedSidecarAuthError— sidecar credentials misconfigured404 Not FoundSidecarConfigurationError— resource not configured on sidecar400 Bad RequestValueErrorwith sidecar error body (e.g., missing AgentIdentity)500 Internal Server ErrorSidecarAuthErrorafter exhausting retriesSidecarUnavailableErrorSidecarAuthErrorwith raw response for diagnosticsRetry and Resilience
Alternatively, use
httpxtransport-level retries or keep retry logic minimal for Phase 1 and rely on the sidecar's own resilience.Token Caching Strategy
force_refreshsupportSecurity Considerations
Network isolation — The sidecar MUST only be accessible from the agent container (same pod in K8s, same Docker bridge network). The SDK MUST validate that the resolved Entra Auth SideCar Base URL points to a loopback address (e.g.,
localhost,127.0.0.1,[::1]) or a private network address (RFC 1918 / RFC 4193). If the resolved URL is a non-loopback, non-private address, the provider MUST raise an error and refuse to issue requests. Exception: This validation MUST be skipped if thesidecar_base_urlis explicitly set in the configuration section of theEntraAuthSideCarentry — an explicit configuration value signals intentional operator override (e.g., for sidecar deployed as a separate container with a routable address within a private network).No credentials in agent config — Unlike the MSAL provider, this provider does NOT require secrets, certificates, or credential configuration.
Token in transit — Communication between SDK and sidecar is over HTTP (not HTTPS) within the pod boundary. This is acceptable because traffic never leaves the pod/node network namespace.
Response validation — Validate that
authorizationHeaderis non-empty and starts withBearerbefore stripping the prefix.Usage Example
Testing Strategy
httpx.AsyncClientto simulate sidecar responses (200, 401, 404, 500, timeout)Open Questions
Should the SDK-side cache read the JWTResolved: Use the existing SDK token expiration management process (same as MSAL provider).expclaim, or rely solely on a configured TTL?ShouldResolved: Use the existing SDK built-in system. Can be revisited in a future phase.get_agentic_instance_tokenandget_agentic_user_tokenalso delegate to the sidecar (future), or always use the existing SDK path after getting the Blueprint token?/healthzfails? Fail-fast (raise) or deferred-failure (fail on first token request)?httpxoraiohttpbe the HTTP client? (httpxrecommended for async-first design and consistent API)References