Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Microsoft 365 Agents SDK for Python - Release Notes v1.4.0 (Unreleased)

## Major Features & Enhancements
- Added support for Workload Identity

## New Models & APIs
- **Regionalized UserTokenClient Support**: Added optional argument to `CloudAdapter` to configure Token Service endpoint used by `RestChannelServiceClientFactory` when creating `UserTokenClient` instances.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,19 @@ def get_assertion() -> str:
)
return result["access_token"]

client_credential = {"client_assertion": get_assertion}
elif self._msal_configuration.AUTH_TYPE == AuthTypes.workload_identity:
Comment thread
rodrigobr-msft marked this conversation as resolved.
if not self._msal_configuration.FEDERATED_TOKEN_FILE:
raise ValueError(
"FEDERATED_TOKEN_FILE must be set in configuration."
)

federated_token_file = self._msal_configuration.FEDERATED_TOKEN_FILE

def get_assertion() -> str:
with open(federated_token_file, encoding="utf-8") as f:
return f.read().strip()

client_credential = {"client_assertion": get_assertion}
else:
logger.error(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"CERTPFXFILE",
"CONNECTIONNAME",
"FEDERATEDCLIENTID",
"FEDERATEDTOKENFILE",
"SCOPES",
"AZUREREGION",
"REGIONALAUTHORITY",
Expand Down Expand Up @@ -81,6 +82,8 @@ class AgentAuthConfiguration:
enforced by JwtTokenValidator (per issue #626) whenever the verified
token's issuer is a recognized Entra issuer with a GUID tenant,
regardless of this flag.
ANONYMOUS_ALLOWED: Whether anonymous access is allowed (default False).
FEDERATED_TOKEN_FILE: The path to the federated token file (if using federated credentials authentication).
"""

TENANT_ID: str | None
Expand All @@ -97,6 +100,7 @@ class AgentAuthConfiguration:
IDPM_RESOURCE: str | None
ANONYMOUS_ALLOWED: bool = False
VALIDATE_ISSUER: bool = False
FEDERATED_TOKEN_FILE: str | None

# Provider-specific settings that aren't first-class fields (e.g. the Entra
# sidecar's SERVICE_NAME, SIDECAR_BASE_URL). Preserved here as a single dict
Expand Down Expand Up @@ -129,6 +133,7 @@ def __init__(
anonymous_allowed: bool | None = None,
issuers: list[str] | None = None,
validate_issuer: bool | None = None,
federated_token_file: str | None = None,
**kwargs: Any,
):

Expand All @@ -148,6 +153,9 @@ def __init__(
self.FEDERATED_CLIENT_ID = federated_client_id or kwargs.get(
"FEDERATEDCLIENTID", None
)
self.FEDERATED_TOKEN_FILE = federated_token_file or kwargs.get(
"FEDERATEDTOKENFILE", None
)
Comment thread
rodrigobr-msft marked this conversation as resolved.
Comment thread
rodrigobr-msft marked this conversation as resolved.
self.SCOPES = scopes or kwargs.get("SCOPES", None)
# Azure regional token service. Falls back to the legacy "REGIONALAUTHORITY"
# configuration key when "AZUREREGION" is not provided.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ class AuthTypes(str, Enum):
federated_credentials = "FederatedCredentials"
identity_proxy_manager = "IdentityProxyManager"
entra_auth_sidecar = "EntraAuthSideCar"
workload_identity = "WorkloadIdentity"
37 changes: 37 additions & 0 deletions tests/authentication_msal/test_msal_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,43 @@ def test_create_client_application_azure_region_defaults_none(self, mocker):
assert mock_cca.call_args.kwargs["azure_region"] is None


class TestMsalAuthWorkloadIdentity:
def test_create_client_application_reads_projected_token(self, mocker, tmp_path):
token_file = tmp_path / "workload-token"
token_file.write_text(" first-token\r\n", encoding="utf-8")
config = AgentAuthConfiguration(
auth_type=AuthTypes.workload_identity,
tenant_id="12345678-1234-1234-1234-123456789abc",
client_id="test-client-id",
federated_token_file=str(token_file),
)
mock_cca = mocker.patch(
"microsoft_agents.authentication.msal.msal_auth.ConfidentialClientApplication"
)

MsalAuth(config)._create_client_application()

client_assertion = mock_cca.call_args.kwargs["client_credential"][
"client_assertion"
]
assert client_assertion() == "first-token"

token_file.write_text("\trefreshed-token\n", encoding="utf-8")
assert client_assertion() == "refreshed-token"

def test_create_client_application_requires_token_file(self):
config = AgentAuthConfiguration(
auth_type=AuthTypes.workload_identity,
tenant_id="12345678-1234-1234-1234-123456789abc",
client_id="test-client-id",
)

with pytest.raises(
ValueError, match="FEDERATED_TOKEN_FILE must be set in configuration"
):
MsalAuth(config)._create_client_application()


class TestMsalAuthIdentityProxyManager:
"""
Test suite for the Identity Proxy Manager (IDPM) authentication type.
Expand Down
15 changes: 15 additions & 0 deletions tests/hosting_core/test_auth_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,21 @@ def test_empty_settings(self):
assert auth_config.SCOPES is None
assert auth_config.AZURE_REGION is None

def test_workload_identity_token_file_from_kwargs(self):
auth_config = AgentAuthConfiguration(
AUTHTYPE="WorkloadIdentity",
CLIENTID="test-client-id",
TENANTID="test-tenant-id",
FEDERATEDTOKENFILE="/var/run/secrets/azure/tokens/azure-identity-token",
)

assert auth_config.AUTH_TYPE == AuthTypes.workload_identity
assert (
auth_config.FEDERATED_TOKEN_FILE
== "/var/run/secrets/azure/tokens/azure-identity-token"
)
assert "FEDERATEDTOKENFILE" not in auth_config.provider_settings

def test_azure_region_from_parameter(self):
auth_config = AgentAuthConfiguration(azure_region="westus")
assert auth_config.AZURE_REGION == "westus"
Expand Down
Loading