Entra ID Auth Sidecar Provider — Microsoft 365 Agents SDK
Summary
Add a new authentication provider to the Microsoft 365 Agents SDK (all languages) that integrates with the Microsoft Entra ID Agent Container (sidecar). This provider implements the existing token provider interfaces by delegating token acquisition to the sidecar's HTTP API rather than directly using MSAL libraries.
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. All credential management is offloaded to the sidecar.
- Simplifies multi-language support — The sidecar exposes a simple HTTP API, removing the need for each language SDK to independently implement complex MSAL flows.
- 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.
By adding this as an authentication provider in the Agents SDK, developers can seamlessly adopt the sidecar pattern while maintaining full compatibility with the SDK's existing agentic identity resolution pipeline.
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 to resolve the Agent Instance identity |
| Agent User Resolution |
Pass the Blueprint token through the existing SDK path to resolve the Agent User identity |
| Token Provider Interface Implementation |
Full implementation of the language-specific token provider interface delegating to the sidecar |
| Connection Manager Implementation |
Connection registry that dispatches to the sidecar provider |
| Configuration model |
Sidecar-specific config using SIDECAR_URL environment variable |
| 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 after Blueprint flow is proven |
Downstream API proxy (/DownstreamApi/* endpoints) |
SDK does not currently proxy API calls through auth providers |
Token validation (/Validate endpoint) |
SDK handles inbound token validation separately |
| OBO Exchange implementation via sidecar |
Deferred to Phase 2 |
Architecture
┌─────────────────────────────────────────────────────────┐
│ Agent Application │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Agents SDK │ │
│ │ │ │
│ │ TokenProvider Interface ◄── SidecarAuthProvider │ │
│ │ │ │ │
│ │ │ GetAgenticApplicationToken() │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ HTTP Client │ ─── HTTP ───┐ │ │
│ │ └─────────────────────────┘ │ │ │
│ └───────────────────────────────────────────┼───────┘ │
│ │ │
└──────────────────────────────────────────────┼───────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ Entra ID Agent Container (Sidecar) :5000 │
│ │
│ GET /AuthorizationHeaderUnauthenticated/{name} │
│ ?AgentIdentity={agent-client-id} │
│ │
│ Response: { "authorizationHeader": "Bearer <token>" } │
│ │
│ Credentials: Managed Identity / Workload Identity / │
│ Client Secret (dev) / Key Vault Cert │
└──────────────────────────────────────────────────────────┘
│
▼
Microsoft Entra ID (login.microsoftonline.com)
Token Flow — Blueprint Identity (Phase 1 Focus)
Agent SDK Sidecar (:5000) Entra ID
│ │ │
│ GetAgenticApplicationToken() │ │
│──────────────────────────────────►│ │
│ GET /AuthorizationHeaderUnauthenticated/default │
│ ?AgentIdentity={agentAppInstanceId} │
│ │ │
│ │ Client Credentials (Blueprint) │
│ │───────────────────────────────►│
│ │◄─────────── T1 (Blueprint) ────│
│ │ │
│ │ FIC Exchange (AgentIdentity) │
│ │───────────────────────────────►│
│ │◄─────────── TR (Agent App) ────│
│ │ │
│◄─── { "authorizationHeader": │ │
│ "Bearer TR" } │ │
│ │ │
│ Parse → strip "Bearer " prefix │ │
│ return TR │ │
│ │ │
│ ─── Existing SDK path ─── │ │
│ GetAgenticInstanceToken() │ │
│ uses TR as client_assertion │ │
│ to acquire Agent Instance token │ │
│ │ │
│ GetAgenticUserToken() │ │
│ 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 Instance and User token resolution already knows how to use a Blueprint token to derive those tokens — this provider just changes how the initial Blueprint token is acquired.
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 |
GetAgenticApplicationToken, GetAgenticInstanceToken, GetAgenticUserToken — mapped to the AgentIdentity query parameter on the sidecar |
| Agent User ID |
The Agent User object ID from the activity request |
GetAgenticUserToken — 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.
Base Interfaces by Language
The new provider implements the existing token provider interface in each language.
.NET
// IAccessTokenProvider — Primary SDK contract
public interface IAccessTokenProvider
{
Task<string> GetAccessTokenAsync(string resourceUrl, IList<string> scopes, bool forceRefresh = false);
TokenCredential GetTokenCredential();
ImmutableConnectionSettings ConnectionSettings { get; }
}
// IAgenticTokenProvider — Agentic/Blueprint identity flows
public interface IAgenticTokenProvider
{
Task<string> GetAgenticApplicationTokenAsync(string tenantId, string agentAppInstanceId, CancellationToken cancellationToken = default);
Task<string> GetAgenticInstanceTokenAsync(string tenantId, string agentAppInstanceId, CancellationToken cancellationToken = default);
Task<string> GetAgenticUserTokenAsync(string tenantId, string agentAppInstanceId, string upn, IList<string> scopes, CancellationToken cancellationToken = default);
}
// IConnections — Named provider registry
public interface IConnections
{
IAccessTokenProvider GetConnection(string name);
bool TryGetConnection(string name, out IAccessTokenProvider connection);
IAccessTokenProvider GetDefaultConnection();
IAccessTokenProvider GetTokenProvider(ClaimsIdentity claimsIdentity, string serviceUrl);
IAccessTokenProvider GetTokenProvider(ClaimsIdentity claimsIdentity, IActivity activity);
}
Python
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()
class Connections(Protocol):
@abstractmethod
def get_connection(self, connection_name: str) -> AccessTokenProviderBase: ...
@abstractmethod
def get_default_connection(self) -> AccessTokenProviderBase: ...
@abstractmethod
def get_token_provider(
self, claims_identity: ClaimsIdentity, service_url: str
) -> AccessTokenProviderBase: ...
@abstractmethod
def get_default_connection_configuration(self) -> AgentAuthConfiguration: ...
TypeScript
export interface AuthProvider {
connectionSettings?: AuthConfiguration
getAccessToken(authConfig: AuthConfiguration, scope: string): Promise<string>
getAccessToken(scope: string): Promise<string>
getAccessToken(authConfigOrScope: AuthConfiguration | string, scope?: string): Promise<string>
getAgenticApplicationToken: (tenantId: string, agentAppInstanceId: string) => Promise<string>
getAgenticInstanceToken: (tenantId: string, agentAppInstanceId: string) => Promise<string>
getAgenticUserToken: (
tenantId: string,
agentAppInstanceId: string,
upn: string,
scopes: string[]
) => Promise<string>
acquireTokenOnBehalfOf(scopes: string[], oboAssertion: string): Promise<string>
acquireTokenOnBehalfOf(authConfig: AuthConfiguration, scopes: string[], oboAssertion: string): Promise<string>
acquireTokenOnBehalfOf(
authConfigOrScopes: AuthConfiguration | string[],
scopesOrOboAssertion?: string[] | string,
oboAssertion?: string
): Promise<string>
}
export interface Connections {
getConnection(name: string): AuthProvider
getDefaultConnection(): AuthProvider
getTokenProvider(identity: JwtPayload, serviceUrl: string): AuthProvider
getTokenProviderFromActivity(identity: JwtPayload, activity: Activity): AuthProvider
getDefaultConnectionConfiguration(): AuthConfiguration
}
Provider Implementation
.NET
Class Declaration
public class SidecarAuth : IAccessTokenProvider, IAgenticTokenProvider
{
private readonly SidecarTokenClient _tokenClient;
private readonly SidecarConnectionSettings _settings;
public SidecarAuth(IServiceProvider serviceProvider, IConfigurationSection configSection)
{
_settings = new SidecarConnectionSettings(configSection);
_tokenClient = serviceProvider.GetRequiredService<SidecarTokenClient>();
}
public ImmutableConnectionSettings ConnectionSettings => new ImmutableConnectionSettings(_settings);
public async Task<string> GetAccessTokenAsync(string resourceUrl, IList<string> scopes, bool forceRefresh = false)
{
var response = await _tokenClient.GetAuthorizationHeaderUnauthenticatedAsync(
agentIdentityClientId: null,
cancellationToken: default
);
return response;
}
public TokenCredential GetTokenCredential()
{
return new SidecarTokenCredential(this);
}
public async Task<string> GetAgenticApplicationTokenAsync(
string tenantId, string agentAppInstanceId, CancellationToken cancellationToken = default)
{
return await _tokenClient.GetAuthorizationHeaderUnauthenticatedAsync(
agentIdentityClientId: agentAppInstanceId,
cancellationToken: cancellationToken
);
}
public async Task<string> GetAgenticInstanceTokenAsync(
string tenantId, string agentAppInstanceId, CancellationToken cancellationToken = default)
{
// Step 1: Get Blueprint token from sidecar
var appToken = await GetAgenticApplicationTokenAsync(tenantId, agentAppInstanceId, cancellationToken);
// Step 2: Existing SDK path — exchange for Instance token
var cca = ConfidentialClientApplicationBuilder
.Create(agentAppInstanceId)
.WithAuthority($"https://login.microsoftonline.com/{tenantId}")
.WithClientAssertion(appToken)
.Build();
var result = await cca
.AcquireTokenForClient(new[] { "api://AzureAdTokenExchange/.default" })
.ExecuteAsync(cancellationToken);
return result.AccessToken;
}
public async Task<string> GetAgenticUserTokenAsync(
string tenantId, string agentAppInstanceId, string upn,
IList<string> scopes, CancellationToken cancellationToken = default)
{
var appToken = await GetAgenticApplicationTokenAsync(tenantId, agentAppInstanceId, cancellationToken);
var instanceToken = await GetAgenticInstanceTokenAsync(tenantId, agentAppInstanceId, cancellationToken);
// Exchange for user token via user_fic grant
var cca = ConfidentialClientApplicationBuilder
.Create(agentAppInstanceId)
.WithAuthority($"https://login.microsoftonline.com/{tenantId}")
.WithClientAssertion(appToken)
.Build();
var result = await cca
.AcquireTokenForClient(scopes)
.WithExtraParameters(new Dictionary<string, string>
{
["user_id"] = upn,
["user_federated_identity_credential"] = instanceToken,
["grant_type"] = "user_fic"
})
.ExecuteAsync(cancellationToken);
return result.AccessToken;
}
}
Internal Token Client
internal class SidecarTokenClient : IDisposable
{
private readonly HttpClient _httpClient;
public SidecarTokenClient(IHttpClientFactory httpClientFactory)
{
_httpClient = httpClientFactory.CreateClient("EntraAuthSideCarClient");
}
public async Task<string> GetAuthorizationHeaderUnauthenticatedAsync(
string agentIdentityClientId,
CancellationToken cancellationToken = default)
{
var url = "/AuthorizationHeaderUnauthenticated/default";
if (!string.IsNullOrEmpty(agentIdentityClientId))
{
url += $"?AgentIdentity={Uri.EscapeDataString(agentIdentityClientId)}";
}
var response = await _httpClient.GetAsync(url, cancellationToken);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync(cancellationToken);
var result = JsonSerializer.Deserialize<SidecarAuthResponse>(content);
var token = result.AuthorizationHeader;
if (token.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
token = token.Substring(7);
}
return token;
}
public async Task<bool> IsHealthyAsync(CancellationToken cancellationToken = default)
{
try
{
var response = await _httpClient.GetAsync("/healthz", cancellationToken);
return response.IsSuccessStatusCode;
}
catch { return false; }
}
}
DI Registration
public static class SidecarServiceCollectionExtensions
{
public static IServiceCollection AddEntraAuthSideCar(
this IServiceCollection services,
IConfiguration configuration,
string configSectionName = "EntraAuthSideCar")
{
var section = configuration.GetSection(configSectionName);
services.Configure<SidecarConnectionSettings>(section);
services.AddHttpClient("EntraAuthSideCarClient", (sp, client) =>
{
var settings = sp.GetRequiredService<IOptions<SidecarConnectionSettings>>().Value;
// Resolution order: SIDECAR_URL env var > SidecarBaseUrl config > default
client.BaseAddress = new Uri(settings.ResolvedSidecarBaseUrl);
client.Timeout = settings.RequestTimeout;
})
.AddTransientHttpErrorPolicy(p =>
p.WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt))));
services.AddSingleton<SidecarTokenClient>();
services.AddSingleton<IAccessTokenProvider, SidecarAuth>();
services.AddSingleton<IAgenticTokenProvider, SidecarAuth>();
return services;
}
}
Python
Class Declaration
import os
import logging
from typing import Optional
import httpx
from msal import ConfidentialClientApplication
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."""
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 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 Blueprint token from sidecar, then existing SDK path.
"""
# 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 {agent_app_instance_id}"
)
# Step 2: Existing SDK path — exchange for Instance token
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 instance token: {result.get('error_description', 'unknown')}"
)
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.
Both agent_app_instance_id and agentic_user_id come from the
inbound ActivityProtocol request.
"""
instance_token, agent_token = await self.get_agentic_instance_token(
tenant_id, agent_app_instance_id
)
# Exchange for user token via user_fic grant
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 user token: {result.get('error_description', 'unknown')}"
)
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."""
auth_header = response_body.get("authorizationHeader", "")
if auth_header.startswith("Bearer "):
return auth_header[7:]
return auth_header
TypeScript
Class Declaration
import { AuthProvider } from './authProvider'
import { AuthConfiguration } from './authConfiguration'
import { ConfidentialClientApplication } from '@azure/msal-node'
import { MemoryCache } from './MemoryCache'
export class SidecarAuthProvider implements AuthProvider {
public readonly connectionSettings?: AuthConfiguration
private readonly _sidecarBaseUrl: string
private readonly _agenticTokenCache: MemoryCache<string>
constructor(connectionSettings?: AuthConfiguration) {
this.connectionSettings = connectionSettings
// Resolution order: SIDECAR_URL env var > sidecarBaseUrl config > default
this._sidecarBaseUrl = process.env.SIDECAR_URL
?? connectionSettings?.sidecarBaseUrl
?? 'http://localhost:5000'
this._agenticTokenCache = new MemoryCache<string>()
}
/**
* Acquire an app-only access token from the sidecar.
*/
async getAccessToken(authConfigOrScope: AuthConfiguration | string, scope?: string): Promise<string> {
const response = await fetch(
`${this._sidecarBaseUrl}/AuthorizationHeaderUnauthenticated/default`
)
if (!response.ok) {
throw new Error(`Sidecar token acquisition failed: ${response.status} ${response.statusText}`)
}
const body = await response.json()
return this._parseToken(body)
}
/**
* Acquire the Blueprint agentic identity token from the sidecar.
* The agentAppInstanceId comes from the inbound ActivityProtocol request.
*/
async getAgenticApplicationToken(tenantId: string, agentAppInstanceId: string): Promise<string> {
const cacheKey = `app/${agentAppInstanceId}`
const cached = this._agenticTokenCache.get(cacheKey)
if (cached) return cached
const url = new URL(`${this._sidecarBaseUrl}/AuthorizationHeaderUnauthenticated/default`)
url.searchParams.set('AgentIdentity', agentAppInstanceId)
const response = await fetch(url.toString())
if (!response.ok) {
throw new Error(`Sidecar agentic app token failed: ${response.status} ${response.statusText}`)
}
const body = await response.json()
const token = this._parseToken(body)
this._agenticTokenCache.set(cacheKey, token, 300)
return token
}
/**
* Acquire the Agent Instance token.
* Uses Blueprint token from sidecar, then existing SDK path.
*/
async getAgenticInstanceToken(tenantId: string, agentAppInstanceId: string): Promise<string> {
const cacheKey = `instance/${tenantId}/${agentAppInstanceId}`
const cached = this._agenticTokenCache.get(cacheKey)
if (cached) return cached
// Step 1: Get Blueprint token from sidecar
const appToken = await this.getAgenticApplicationToken(tenantId, agentAppInstanceId)
// Step 2: Existing SDK path — exchange for Instance token
const cca = new ConfidentialClientApplication({
auth: {
clientId: agentAppInstanceId,
authority: `https://login.microsoftonline.com/${tenantId}`,
clientAssertion: appToken
}
})
const result = await cca.acquireTokenByClientCredential({
scopes: ['api://AzureAdTokenExchange/.default']
})
if (!result?.accessToken) {
throw new Error(`Failed to acquire agentic instance token for ${agentAppInstanceId}`)
}
const ttl = (result.expiresOn?.getTime() ?? Date.now() + 300000 - Date.now()) / 1000 - 300
this._agenticTokenCache.set(cacheKey, result.accessToken, ttl)
return result.accessToken
}
/**
* Acquire a user-scoped token using the agentic identity chain.
* Both agentAppInstanceId and agenticUserId come from the ActivityProtocol request.
*/
async getAgenticUserToken(
tenantId: string,
agentAppInstanceId: string,
agenticUserId: string,
scopes: string[]
): Promise<string> {
const appToken = await this.getAgenticApplicationToken(tenantId, agentAppInstanceId)
const instanceToken = await this.getAgenticInstanceToken(tenantId, agentAppInstanceId)
// Exchange for user token via user_fic grant
const authority = `https://login.microsoftonline.com/${tenantId}`
const body = new URLSearchParams({
client_id: agentAppInstanceId,
scope: scopes.join(' '),
grant_type: 'user_fic',
user_id: agenticUserId,
user_federated_identity_credential: instanceToken,
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
client_assertion: appToken
})
const response = await fetch(`${authority}/oauth2/v2.0/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8' },
body: body.toString()
})
if (!response.ok) {
const errorBody = await response.text()
throw new Error(`Failed to acquire agentic user token: ${errorBody}`)
}
const result = await response.json()
return result.access_token
}
/**
* OBO flow — not supported in Phase 1.
*/
async acquireTokenOnBehalfOf(
authConfigOrScopes: AuthConfiguration | string[],
scopesOrOboAssertion?: string[] | string,
oboAssertion?: string
): Promise<string> {
throw new Error('acquireTokenOnBehalfOf not supported by Sidecar provider in Phase 1')
}
async isHealthy(): Promise<boolean> {
try {
const response = await fetch(`${this._sidecarBaseUrl}/healthz`)
return response.ok
} catch { return false }
}
private _parseToken(responseBody: { authorizationHeader?: string }): string {
const authHeader = responseBody.authorizationHeader ?? ''
if (authHeader.startsWith('Bearer ')) {
return authHeader.substring(7)
}
if (!authHeader) {
throw new Error('Sidecar response missing authorizationHeader field')
}
return authHeader
}
}
Connection Manager
.NET
The .NET SDK uses IConnections for connection dispatch. The sidecar provider registers via DI and the existing ConfigurationConnections class resolves providers by name. No custom connection manager is needed — just DI registration (see DI Registration above).
The CloudAdapter resolves the token provider via:
var tokenProvider = connections.GetTokenProvider(claimsIdentity, activity);
Python
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 provider_factory parameter. This enables support for additional auth providers in the future without duplicating connection routing logic.
Rationale: The current MsalConnectionManager contains ~140 lines of connection routing logic (audience matching, service URL regex dispatch) that is entirely provider-agnostic. The only MSAL-specific line is MsalAuth(config). Refactoring avoids duplication for every new auth provider.
import re
from typing import Optional, Dict, List, Callable
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 determines which auth provider is
instantiated per connection, enabling future providers without new managers.
"""
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
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:
from microsoft_agents.hosting.core import ConnectionManager
from microsoft_agents.authentication.entra_sidecar import SidecarAuth
connection_manager = ConnectionManager(
provider_factory=SidecarAuth,
CONNECTIONS=agents_sdk_config["CONNECTIONS"],
)
Backward-compatible MsalConnectionManager:
class MsalConnectionManager(ConnectionManager):
def __init__(self, connections_configurations=None, connections_map=None, **kwargs):
super().__init__(provider_factory=MsalAuth, connections_configurations=connections_configurations, connections_map=connections_map, **kwargs)
TypeScript
Same approach as Python — refactor MsalConnectionManager into a generic ConnectionManager that accepts a provider factory.
import { JwtPayload } from 'jsonwebtoken'
import { Activity, RoleTypes } from '@microsoft/agents-activity'
import { Connections } from './connections'
import { AuthProvider } from './authProvider'
import { AuthConfiguration, ConnectionMapItem } from './authConfiguration'
export type AuthProviderFactory = (config: AuthConfiguration) => AuthProvider
export class ConnectionManager implements Connections {
private _connections: Map<string, AuthProvider> = new Map()
private _connectionsMap: ConnectionMapItem[] = []
private _serviceConnectionConfiguration: AuthConfiguration
private static readonly DEFAULT_CONNECTION = 'serviceConnection'
constructor (
providerFactory: AuthProviderFactory,
connectionsConfigurations: Map<string, AuthConfiguration> = new Map(),
connectionsMap: ConnectionMapItem[] = [],
configuration: AuthConfiguration = {}
) {
this._serviceConnectionConfiguration = {}
const providedConnections = connectionsConfigurations.size > 0
? connectionsConfigurations
: (configuration.connections || new Map())
this._connectionsMap = connectionsMap.length > 0
? connectionsMap
: (configuration.connectionsMap || [])
for (const [name, config] of providedConnections) {
this._connections.set(name, providerFactory(config))
if (name === ConnectionManager.DEFAULT_CONNECTION) {
this._serviceConnectionConfiguration = config
}
}
if (!this._connections.has(ConnectionManager.DEFAULT_CONNECTION)) {
this._connections.set(ConnectionManager.DEFAULT_CONNECTION, providerFactory(configuration))
this._serviceConnectionConfiguration = configuration
}
}
getConnection (name: string): AuthProvider {
const conn = this._connections.get(name)
if (!conn) throw new Error(`Connection not found: ${name}`)
return conn
}
getDefaultConnection (): AuthProvider { return this.getConnection(ConnectionManager.DEFAULT_CONNECTION) }
getTokenProvider (identity: JwtPayload, serviceUrl: string): AuthProvider {
const audience = Array.isArray(identity?.aud) ? identity.aud[0] : identity.aud
for (const item of this._connectionsMap) {
let audienceMatch = true
if (item.audience && audience) audienceMatch = item.audience === audience
if (audienceMatch) {
if (item.serviceUrl === '*' || !item.serviceUrl) return this.getConnection(item.connection)
if (new RegExp(item.serviceUrl, 'i').test(serviceUrl)) return this.getConnection(item.connection)
}
}
return this.getDefaultConnection()
}
getTokenProviderFromActivity (identity: JwtPayload, activity: Activity): AuthProvider {
const connection = this.getTokenProvider(identity, activity.serviceUrl || '')
if (activity.recipient?.role === RoleTypes.AgenticIdentity || activity.recipient?.role === RoleTypes.AgenticUser) {
const altName = connection.connectionSettings?.altBlueprintConnectionName?.trim()
if (altName) return this.getConnection(altName)
}
return connection
}
getDefaultConnectionConfiguration (): AuthConfiguration { return this._serviceConnectionConfiguration }
}
Usage with Sidecar:
import { ConnectionManager } from '@microsoft/agents-hosting'
import { SidecarAuthProvider } from './sidecarAuthProvider'
const adapter = new CloudAdapter(authConfig)
adapter.connectionManager = new ConnectionManager(
(config) => new SidecarAuthProvider(config),
undefined, undefined, authConfig
)
Backward-compatible MsalConnectionManager:
export class MsalConnectionManager extends ConnectionManager {
constructor (connectionsConfigurations?, connectionsMap?, configuration?) {
super((config) => new MsalTokenProvider(config), connectionsConfigurations, connectionsMap, configuration)
}
}
Configuration
Environment Variables (All Languages)
| Variable |
Required |
Default |
Description |
SIDECAR_URL |
No |
http://localhost:5000 |
Sidecar HTTP endpoint. Takes precedence over any SidecarBaseUrl / sidecarBaseUrl / sidecar_base_url config setting if both are present. |
URL Resolution Precedence (all languages):
SIDECAR_URL environment variable (highest priority)
SidecarBaseUrl from SDK configuration (optional)
- Default:
http://localhost:5000
SDK Configuration
Required fields (all languages): Only ClientId and Scopes are required. All other settings are optional and have sensible defaults.
.NET
{
"Connections": {
"BotServiceConnection": {
"Type": "EntraAuthSideCar",
"Settings": {
"ClientId": "00000000-0000-0000-0000-000000000000",
"Scopes": [ "api://00000000-0000-0000-0000-000000000000/.default" ],
"SidecarBaseUrl": "http://localhost:5000",
"RequestTimeout": "00:00:30",
"RetryCount": 3
}
}
}
}
Environment variable style configuration (double-underscore pattern):
Connections__BotServiceConnection__Type=EntraAuthSideCar
Connections__BotServiceConnection__Settings__ClientId=00000000-0000-0000-0000-000000000000
Connections__BotServiceConnection__Settings__Scopes__0=api://00000000-0000-0000-0000-000000000000/.default
Connections__BotServiceConnection__Settings__SidecarBaseUrl=http://my-sidecar:5000 # optional config fallback
Connections__BotServiceConnection__Settings__RequestTimeout=00:00:30
Connections__BotServiceConnection__Settings__RetryCount=3
SIDECAR_URL=http://localhost:5000 # takes precedence over SidecarBaseUrl if set
Configuration model:
public class SidecarConnectionSettings : ConnectionSettingsBase
{
/// <summary>
/// OAuth scopes to request from the sidecar.
/// Required.
/// </summary>
public IList<string> Scopes { get; set; } = new List<string>();
/// <summary>
/// Base URL of the Entra ID Agent Container (sidecar).
/// Optional. If set, provides a fallback URL for the sidecar endpoint.
/// Resolution order: SIDECAR_URL env var > this setting > default (http://localhost:5000).
/// </summary>
public string? SidecarBaseUrl { get; set; }
/// <summary>
/// Resolved base URL taking precedence rules into account.
/// Priority: SIDECAR_URL env var > SidecarBaseUrl config > "http://localhost:5000"
/// </summary>
public string ResolvedSidecarBaseUrl =>
Environment.GetEnvironmentVariable("SIDECAR_URL")
?? SidecarBaseUrl
?? "http://localhost:5000";
/// <summary>
/// HTTP request timeout for sidecar calls. Default: 30 seconds.
/// </summary>
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(30);
/// <summary>
/// Number of retry attempts for transient failures. Default: 3.
/// </summary>
public int RetryCount { get; set; } = 3;
}
Python
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
Or programmatic:
from microsoft_agents.hosting.core import AgentAuthConfiguration
config = AgentAuthConfiguration(
auth_type="EntraAuthSideCar",
client_id="<blueprint-app-id>",
scopes=["api://<blueprint-app-id>/.default"],
sidecar_base_url="http://localhost:5000", # optional; SIDECAR_URL env var takes precedence
)
manager = SidecarConnectionManager(
connections_configurations={"SERVICE_CONNECTION": config}
)
New AuthTypes enum value:
class AuthTypes(str, Enum):
# ... existing values ...
entra_auth_sidecar = "EntraAuthSideCar" # NEW — Entra ID Agent Container
TypeScript
// Programmatic
const authConfig: AuthConfiguration = {
authType: AuthType.EntraAuthSideCar,
clientId: process.env.BLUEPRINT_APP_ID,
scope: 'api://<blueprint-app-id>/.default',
sidecarBaseUrl: 'http://localhost:5000', // optional; SIDECAR_URL env var takes precedence
connections: new Map([
['serviceConnection', {
authType: AuthType.EntraAuthSideCar,
clientId: process.env.BLUEPRINT_APP_ID,
scope: 'api://<blueprint-app-id>/.default',
sidecarBaseUrl: 'http://localhost:5000', // optional; SIDECAR_URL env var takes precedence
}]
]),
connectionsMap: [{ serviceUrl: '*', connection: 'serviceConnection' }]
}
// Or environment variable style (double-underscore pattern)
// connections__serviceConnection__authType=EntraAuthSideCar
// connections__serviceConnection__clientId=<blueprint-app-id>
// connections__serviceConnection__scope=api://<blueprint-app-id>/.default
// connections__serviceConnection__sidecarBaseUrl=http://my-sidecar:5000 (optional config fallback)
// connectionsMap__0__serviceUrl=*
// connectionsMap__0__connection=serviceConnection
// SIDECAR_URL=http://localhost:5000 (takes precedence over sidecarBaseUrl if set)
New AuthType enum value:
export enum AuthType {
// ... existing values ...
EntraAuthSideCar = 'EntraAuthSideCar' // NEW — Entra ID Agent Container
}
Package Structure
.NET
New NuGet package: Microsoft.Agents.Authentication.EntraAuthSideCar
src/libraries/Authentication/Authentication.EntraAuthSideCar/
├── Microsoft.Agents.Authentication.EntraAuthSideCar.csproj
├── SidecarAuth.cs
├── SidecarTokenClient.cs
├── SidecarTokenCredential.cs
├── SidecarConnectionSettings.cs
├── SidecarServiceCollectionExtensions.cs
└── Model/
└── SidecarAuthResponse.cs
Dependencies:
Microsoft.Agents.Authentication (project ref — core interfaces)
Microsoft.Extensions.Http (IHttpClientFactory)
Microsoft.Extensions.Http.Polly (retry policies)
Python
New package: microsoft-agents-authentication-entra-sidecar
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
Additionally, a generic ConnectionManager is added to microsoft-agents-hosting-core:
libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/
├── connection_manager.py (NEW — generic provider-agnostic connection manager)
└── ...
Note: No sidecar-specific connection manager is needed. The generic ConnectionManager accepts a provider_factory callable, enabling any AccessTokenProviderBase implementation (Sidecar, MSAL, future providers) to be used without duplicating routing logic.
Dependencies:
microsoft-agents-hosting-core (core interfaces + generic ConnectionManager)
httpx>=0.27.0 (async HTTP client)
TypeScript
New files within existing @microsoft/agents-hosting package (no separate package):
packages/agents-hosting/src/auth/
├── connectionManager.ts (NEW — generic provider-agnostic connection manager)
├── sidecarAuthProvider.ts (NEW)
├── msalConnectionManager.ts (existing — refactored to extend ConnectionManager)
└── index.ts (updated — add exports)
Note: No sidecarConnectionManager.ts is needed. Use the generic ConnectionManager with a SidecarAuthProvider factory.
No new npm dependencies — uses native fetch (Node.js 20+ built-in). Existing @azure/msal-node is used for Instance/User token exchange.
Error Handling
| Sidecar Response |
SDK Behavior |
200 OK + valid JSON |
Parse authorizationHeader, strip "Bearer " prefix, return token |
401 Unauthorized |
Throw auth error — sidecar credentials misconfigured |
404 Not Found |
Throw config error — resource not configured on sidecar |
400 Bad Request |
Throw argument error with sidecar error body (e.g., missing AgentIdentity) |
500 Internal Server Error |
Retry per policy; throw auth error after exhausting retries |
| Connection refused / timeout |
Retry per policy; throw unavailable error |
| Non-JSON response |
Throw auth error with raw response for diagnostics |
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) |
forceRefresh 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 SidecarBaseUrl 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 providers, 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. 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.
Sidecar Configuration Reference (for container setup — not SDK config)
| Variable |
Required |
Description |
AzureAd__Instance |
Yes |
https://login.microsoftonline.com/ |
AzureAd__TenantId |
Yes |
Tenant ID |
AzureAd__ClientId |
Yes |
Blueprint app registration client ID |
AzureAd__ClientCredentials__0__SourceType |
Yes |
Credential type (ClientSecret, SignedAssertionFilePath, etc.) |
AzureAd__ClientCredentials__0__ClientSecret |
Dev only |
Client secret value |
DownstreamApis__{name}__BaseUrl |
Per API |
Base URL of downstream API (sidecar-side config only) |
DownstreamApis__{name}__Scopes__0 |
Per API |
OAuth scope for downstream API (sidecar-side config only) |
DownstreamApis__{name}__RequestAppToken |
Per API |
true for app-only tokens (sidecar-side config only) |
Testing Strategy
| Test Type |
Description |
| Unit tests |
Mock HTTP client 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 Instance/User token methods also delegate to the sidecar, or use the existing SDK path? 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 (throw) or deferred-failure (fail on first token request)? |
Developer experience |
| 4 |
Does the sidecar return token expiry metadata or should the SDK always parse the JWT? |
Token caching implementation |
| 5 |
(TypeScript) Should this be a separate npm package or new files in the existing @microsoft/agents-hosting? |
Package architecture |
References
Entra ID Auth Sidecar Provider — Microsoft 365 Agents SDK
Summary
Add a new authentication provider to the Microsoft 365 Agents SDK (all languages) that integrates with the Microsoft Entra ID Agent Container (sidecar). This provider implements the existing token provider interfaces by delegating token acquisition to the sidecar's HTTP API rather than directly using MSAL libraries.
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:
By adding this as an authentication provider in the Agents SDK, developers can seamlessly adopt the sidecar pattern while maintaining full compatibility with the SDK's existing agentic identity resolution pipeline.
Scope
In Scope (Phase 1)
GET /AuthorizationHeaderUnauthenticated/{name}SIDECAR_URLenvironment variableGET /healthzat startupOut of Scope (Future Phases)
/DownstreamApi/*endpoints)/Validateendpoint)Architecture
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 Instance and User token resolution already knows how to use a Blueprint token to derive those tokens — this provider just changes how the initial Blueprint token is acquired.
Parameter 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:GetAgenticApplicationToken,GetAgenticInstanceToken,GetAgenticUserToken— mapped to theAgentIdentityquery parameter on the sidecarGetAgenticUserToken— 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.
Base Interfaces by Language
The new provider implements the existing token provider interface in each language.
.NET
Python
TypeScript
Provider Implementation
.NET
Class Declaration
Internal Token Client
DI Registration
Python
Class Declaration
TypeScript
Class Declaration
Connection Manager
.NET
The .NET SDK uses
IConnectionsfor connection dispatch. The sidecar provider registers via DI and the existingConfigurationConnectionsclass resolves providers by name. No custom connection manager is needed — just DI registration (see DI Registration above).The
CloudAdapterresolves the token provider via:Python
Rather than creating a sidecar-specific connection manager, the existing
MsalConnectionManagershould be refactored into a genericConnectionManagerthat accepts anyAccessTokenProviderBaseimplementation via aprovider_factoryparameter. This enables support for additional auth providers in the future without duplicating connection routing logic.Rationale: The current
MsalConnectionManagercontains ~140 lines of connection routing logic (audience matching, service URL regex dispatch) that is entirely provider-agnostic. The only MSAL-specific line isMsalAuth(config). Refactoring avoids duplication for every new auth provider.Usage with Sidecar:
Backward-compatible
MsalConnectionManager:TypeScript
Same approach as Python — refactor
MsalConnectionManagerinto a genericConnectionManagerthat accepts a provider factory.Usage with Sidecar:
Backward-compatible
MsalConnectionManager:Configuration
Environment Variables (All Languages)
SIDECAR_URLhttp://localhost:5000SidecarBaseUrl/sidecarBaseUrl/sidecar_base_urlconfig setting if both are present.URL Resolution Precedence (all languages):
SIDECAR_URLenvironment variable (highest priority)SidecarBaseUrlfrom SDK configuration (optional)http://localhost:5000SDK Configuration
Required fields (all languages): Only
ClientIdandScopesare required. All other settings are optional and have sensible defaults..NET
{ "Connections": { "BotServiceConnection": { "Type": "EntraAuthSideCar", "Settings": { "ClientId": "00000000-0000-0000-0000-000000000000", "Scopes": [ "api://00000000-0000-0000-0000-000000000000/.default" ], "SidecarBaseUrl": "http://localhost:5000", "RequestTimeout": "00:00:30", "RetryCount": 3 } } } }Environment variable style configuration (double-underscore pattern):
Configuration model:
Python
Environment variable style configuration (double-underscore pattern):
Or programmatic:
New
AuthTypesenum value:TypeScript
New
AuthTypeenum value:Package Structure
.NET
New NuGet package:
Microsoft.Agents.Authentication.EntraAuthSideCarDependencies:
Microsoft.Agents.Authentication(project ref — core interfaces)Microsoft.Extensions.Http(IHttpClientFactory)Microsoft.Extensions.Http.Polly(retry policies)Python
New package:
microsoft-agents-authentication-entra-sidecarAdditionally, a generic
ConnectionManageris added tomicrosoft-agents-hosting-core:Dependencies:
microsoft-agents-hosting-core(core interfaces + genericConnectionManager)httpx>=0.27.0(async HTTP client)TypeScript
New files within existing
@microsoft/agents-hostingpackage (no separate package):No new npm dependencies — uses native
fetch(Node.js 20+ built-in). Existing@azure/msal-nodeis used for Instance/User token exchange.Error Handling
200 OK+ valid JSONauthorizationHeader, strip"Bearer "prefix, return token401 Unauthorized404 Not Found400 Bad Request500 Internal Server ErrorToken Caching Strategy
forceRefreshsupportSecurity 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 theSidecarBaseUrlis 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 providers, 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. Acceptable because traffic never leaves the pod/node network namespace.
Response validation — Validate that
authorizationHeaderis non-empty and starts withBearerbefore stripping the prefix.Sidecar Configuration Reference (for container setup — not SDK config)
AzureAd__Instancehttps://login.microsoftonline.com/AzureAd__TenantIdAzureAd__ClientIdAzureAd__ClientCredentials__0__SourceTypeClientSecret,SignedAssertionFilePath, etc.)AzureAd__ClientCredentials__0__ClientSecretDownstreamApis__{name}__BaseUrlDownstreamApis__{name}__Scopes__0DownstreamApis__{name}__RequestAppTokentruefor app-only tokens (sidecar-side config only)Testing Strategy
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?Should Instance/User token methods also delegate to the sidecar, or use the existing SDK path?Resolved: Use the existing SDK built-in system. Can be revisited in a future phase./healthzfails? Fail-fast (throw) or deferred-failure (fail on first token request)?@microsoft/agents-hosting?References