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
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,11 @@ def _resolve_authority(
)

if config.AUTHORITY:
return re.sub(r"/common(?=/|$)", f"/{tenant_id}", config.AUTHORITY)
return re.sub(
r"/(?:common|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})(?=/|$)",
f"/{tenant_id}",
config.AUTHORITY,
)

return f"https://login.microsoftonline.com/{tenant_id}"

Expand All @@ -177,7 +181,7 @@ def _resolve_tenant_id(
return tenant_id
raise ValueError("TENANT_ID is not set in the configuration.")

if tenant_id and config.TENANT_ID.lower() == "common":
if tenant_id or config.TENANT_ID.lower() == "common":
return tenant_id

return config.TENANT_ID
Expand Down Expand Up @@ -366,7 +370,7 @@ async def get_agentic_instance_token(
client_id=agent_app_instance_id,
authority=authority,
client_credential={"client_assertion": agent_token_result},
token_cache=self._token_cache,
# token_cache=self._token_cache,
)

agentic_instance_token = await _async_acquire_token_for_client(
Expand Down Expand Up @@ -458,7 +462,7 @@ async def get_agentic_user_token(
client_id=agent_app_instance_id,
authority=authority,
client_credential={"client_assertion": agent_token},
token_cache=self._token_cache,
# token_cache=self._token_cache,
)

logger.info(
Expand Down
2 changes: 1 addition & 1 deletion libraries/microsoft-agents-authentication-msal/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
version=package_version,
install_requires=[
f"microsoft-agents-hosting-core=={package_version}",
"msal>=1.31.1",
"msal>=1.34.0",
"requests>=2.32.3",
"cryptography>=44.0.0",
],
Expand Down
54 changes: 54 additions & 0 deletions test_samples/agentic-test/src/JWTDecodeCard.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"type": "AdaptiveCard",
"$schema": "https://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.6",
"body": [
{
"type": "TextBlock",
"text": "Agentic Message Received - Token Decode",
"wrap": true,
"weight": "Bolder"
},
{
"type": "TextBlock",
"text": "Token Decode:",
"wrap": true
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"width": "stretch",
"items": [
{
"type": "FactSet",
"facts": [
{
"title": "Token Length",
"value": "{{length}}"
},
{
"title": "Name",
"value": "{{name}}"
},
{
"title": "Upn",
"value": "{{upn}}"
},
{
"title": "Oid",
"value": "{{oid}}"
},
{
"title": "Tid",
"value": "{{tid}}"
}
]
}
]
}
]
}
]
}
25 changes: 24 additions & 1 deletion test_samples/agentic-test/src/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# Licensed under the MIT License.

import logging
import os
import jwt
from dotenv import load_dotenv

from os import environ
Expand All @@ -12,13 +14,16 @@
TurnState,
TurnContext,
MemoryStorage,
MessageFactory,
)
from microsoft_agents.hosting.core.storage import (
TranscriptLoggerMiddleware,
ConsoleTranscriptLogger,
)
from microsoft_agents.authentication.msal import MsalConnectionManager
from microsoft_agents.activity import load_configuration_from_env
from microsoft_agents.activity import load_configuration_from_env, Attachment

from jwtcard import load_adaptive_card, update_card_data

logger = logging.getLogger(__name__)

Expand All @@ -38,7 +43,25 @@

@AGENT_APP.activity("message", auth_handlers=["AGENTIC"])
async def on_message(context: TurnContext, _state: TurnState):

aau_token = await AGENT_APP.auth.get_token(context, "AGENTIC")
decoded = jwt.decode(aau_token.token, options={"verify_signature": False})
decoded["length"] = len(aau_token.token)

relative_path = os.path.abspath(os.path.dirname(__file__))
template_path = os.path.join(relative_path, "JWTDecodeCard.json")

card = load_adaptive_card(template_path)
populated_card = update_card_data(card, decoded)

attachment = MessageFactory.attachment(
Attachment(
content_type="application/vnd.microsoft.card.adaptive",
content=populated_card,
)
)
await context.send_activity(attachment)

await context.send_activity(
f"Acquired agentic user token with length: {len(aau_token.token)}"
)
Expand Down
29 changes: 29 additions & 0 deletions test_samples/agentic-test/src/jwtcard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import json
import os


def load_adaptive_card(file_path):
"""Load Adaptive Card JSON from a file with validation."""
if not os.path.exists(file_path):
raise FileNotFoundError(f"Adaptive Card file not found: {file_path}")

with open(file_path, "r", encoding="utf-8") as file:
try:
card_json = json.load(file)
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in {file_path}: {e}")

return card_json


def update_card_data(card_json, data_map):
"""
Replace placeholders in the Adaptive Card JSON with actual values.
Placeholders should be in the form {{key}} in the JSON template.
"""
card_str = json.dumps(card_json) # Convert to string for replacement
for key, value in data_map.items():
placeholder = f"{{{{{key}}}}}" # e.g., {{name}}
card_str = card_str.replace(placeholder, str(value))

return json.loads(card_str) # Convert back to dict
4 changes: 2 additions & 2 deletions test_samples/agentic-test/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
ms_agents_logger.setLevel(logging.DEBUG)


from .agent import AGENT_APP, CONNECTION_MANAGER
from .start_server import start_server
from agent import AGENT_APP, CONNECTION_MANAGER
from start_server import start_server

start_server(
agent_application=AGENT_APP,
Expand Down
150 changes: 150 additions & 0 deletions tests/authentication_msal/test_msal_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

from microsoft_agents.authentication.msal import MsalAuth
from microsoft_agents.hosting.core import Connections
from microsoft_agents.hosting.core.authorization import (
AgentAuthConfiguration,
AuthTypes,
)

from tests._common.testing_objects import MockMsalAuth

Expand Down Expand Up @@ -63,6 +67,152 @@ async def test_acquire_token_on_behalf_of_confidential(self, mocker):
)


class TestMsalAuthTenantResolution:
"""
Test suite for testing tenant resolution methods in MsalAuth.
These methods are critical for multi-tenant authentication support.
"""

def test_resolve_tenant_id_with_override_parameter(self):
"""Test that tenant_id parameter takes precedence when provided"""
config = AgentAuthConfiguration(
tenant_id="12345678-1234-1234-1234-123456789abc"
)
result = MsalAuth._resolve_tenant_id(config, "tenant-override")
assert result == "tenant-override"

def test_resolve_tenant_id_with_common_and_tenant_parameter(self):
"""Test that tenant_id parameter is used when config.TENANT_ID is 'common'"""
config = AgentAuthConfiguration(tenant_id="common")
result = MsalAuth._resolve_tenant_id(config, "specific-tenant")
assert result == "specific-tenant"

def test_resolve_tenant_id_with_common_no_tenant_parameter(self):
"""Test that None is returned when config.TENANT_ID is 'common' and no tenant_id provided"""
config = AgentAuthConfiguration(tenant_id="common")
result = MsalAuth._resolve_tenant_id(config, None)
assert result is None

def test_resolve_tenant_id_with_specific_tenant(self):
"""Test that config.TENANT_ID is returned when it's a specific value"""
config = AgentAuthConfiguration(
tenant_id="12345678-1234-1234-1234-123456789abc"
)
result = MsalAuth._resolve_tenant_id(config, None)
assert result == "12345678-1234-1234-1234-123456789abc"

def test_resolve_tenant_id_no_config_tenant_with_parameter(self):
"""Test that tenant_id parameter is used when config.TENANT_ID is not set.
Note: tenant_id can be any string, not just GUID format."""
config = AgentAuthConfiguration()
result = MsalAuth._resolve_tenant_id(config, "fallback-tenant")
assert result == "fallback-tenant"

def test_resolve_tenant_id_no_config_tenant_no_parameter(self):
"""Test that ValueError is raised when neither config.TENANT_ID nor tenant_id are set"""
config = AgentAuthConfiguration()
with pytest.raises(
ValueError, match="TENANT_ID is not set in the configuration"
):
MsalAuth._resolve_tenant_id(config, None)

def test_resolve_authority_with_common_replacement(self):
"""Test that /common is replaced with the resolved tenant_id in authority URL"""
config = AgentAuthConfiguration(
tenant_id="12345678-1234-1234-1234-123456789abc",
authority="https://login.microsoftonline.com/common",
)
result = MsalAuth._resolve_authority(config, None)
assert (
result
== "https://login.microsoftonline.com/12345678-1234-1234-1234-123456789abc"
)

def test_resolve_authority_with_tenant_guid_replacement(self):
"""Test that existing tenant GUID is replaced with new tenant_id in authority URL"""
config = AgentAuthConfiguration(
tenant_id="12345678-1234-1234-1234-123456789abc",
authority="https://login.microsoftonline.com/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
)
result = MsalAuth._resolve_authority(
config, "new-tenant-11111111-2222-3333-4444-555555555555"
)
assert (
result
== "https://login.microsoftonline.com/new-tenant-11111111-2222-3333-4444-555555555555"
)

def test_resolve_authority_with_common_and_tenant_parameter(self):
"""Test that /common is replaced with provided tenant_id parameter"""
config = AgentAuthConfiguration(
tenant_id="common", authority="https://login.microsoftonline.com/common"
)
result = MsalAuth._resolve_authority(
config, "override-22222222-3333-4444-5555-666666666666"
)
assert (
result
== "https://login.microsoftonline.com/override-22222222-3333-4444-5555-666666666666"
)

def test_resolve_authority_no_authority_configured(self):
"""Test fallback to default URL when no authority is configured"""
config = AgentAuthConfiguration(
tenant_id="12345678-1234-1234-1234-123456789abc"
)
result = MsalAuth._resolve_authority(config, None)
assert (
result
== "https://login.microsoftonline.com/12345678-1234-1234-1234-123456789abc"
)

def test_resolve_authority_no_authority_with_tenant_override(self):
"""Test fallback to default URL with tenant override when no authority is configured"""
config = AgentAuthConfiguration(
tenant_id="12345678-1234-1234-1234-123456789abc"
)
result = MsalAuth._resolve_authority(
config, "override-99999999-8888-7777-6666-555555555555"
)
assert (
result
== "https://login.microsoftonline.com/override-99999999-8888-7777-6666-555555555555"
)

def test_resolve_authority_with_common_no_tenant_parameter(self):
"""Test behavior when config.TENANT_ID is 'common' and no tenant_id parameter"""
config = AgentAuthConfiguration(
tenant_id="common", authority="https://login.microsoftonline.com/common"
)
# When tenant_id is None after resolution, should return original authority
result = MsalAuth._resolve_authority(config, None)
assert result == "https://login.microsoftonline.com/common"

def test_resolve_authority_regex_with_trailing_slash(self):
"""Test that regex correctly handles authority URLs with trailing slashes"""
config = AgentAuthConfiguration(
tenant_id="12345678-1234-1234-1234-123456789abc",
authority="https://login.microsoftonline.com/common/",
)
result = MsalAuth._resolve_authority(config, None)
assert (
result
== "https://login.microsoftonline.com/12345678-1234-1234-1234-123456789abc/"
)

def test_resolve_authority_regex_preserves_path(self):
"""Test that regex correctly replaces tenant while preserving additional path segments"""
config = AgentAuthConfiguration(
tenant_id="12345678-1234-1234-1234-123456789abc",
authority="https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
)
result = MsalAuth._resolve_authority(config, None)
assert (
result
== "https://login.microsoftonline.com/12345678-1234-1234-1234-123456789abc/oauth2/v2.0/authorize"
)


# class TestMsalAuthAgentic:

# @pytest.mark.asyncio
Expand Down