Skip to content
Closed
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
9 changes: 8 additions & 1 deletion packages/api/src/microsoft/teams/api/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@
"""

from .caller import CallerIds, CallerType
from .credentials import ClientCredentials, Credentials, ManagedIdentityCredentials, TokenCredentials
from .credentials import (
CertificateCredentials,
ClientCredentials,
Credentials,
ManagedIdentityCredentials,
TokenCredentials,
)
from .json_web_token import JsonWebToken, JsonWebTokenPayload
from .token import TokenProtocol

__all__ = [
"CallerIds",
"CallerType",
"CertificateCredentials",
"ClientCredentials",
"Credentials",
"ManagedIdentityCredentials",
Expand Down
23 changes: 22 additions & 1 deletion packages/api/src/microsoft/teams/api/auth/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,27 @@ class TokenCredentials(CustomBaseModel):
"""


class CertificateCredentials(CustomBaseModel):
"""Credentials for authentication using X.509 certificate (PEM format)."""

client_id: str
"""
The client ID.
"""
private_key: str
"""
The private key in PEM format.
"""
thumbprint: str
"""
The SHA-1 thumbprint of the certificate.
"""
tenant_id: Optional[str] = None
"""
The tenant ID. This should only be passed in for single tenant apps.
"""


class ManagedIdentityCredentials(CustomBaseModel):
"""Credentials for authentication using Azure Managed Identity."""

Expand All @@ -61,4 +82,4 @@ class ManagedIdentityCredentials(CustomBaseModel):


# Union type for credentials
Credentials = Union[ClientCredentials, TokenCredentials, ManagedIdentityCredentials]
Credentials = Union[ClientCredentials, TokenCredentials, CertificateCredentials, ManagedIdentityCredentials]
15 changes: 15 additions & 0 deletions packages/apps/src/microsoft/teams/apps/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
ActivityBase,
ActivityParams,
ApiClient,
CertificateCredentials,
ClientCredentials,
ConversationAccount,
ConversationReference,
Expand Down Expand Up @@ -290,6 +291,10 @@ def _init_credentials(self) -> Optional[Credentials]:
client_id = self.options.client_id or os.getenv("CLIENT_ID")
client_secret = self.options.client_secret or os.getenv("CLIENT_SECRET")
tenant_id = self.options.tenant_id or os.getenv("TENANT_ID")
certificate_private_key_path = self.options.certificate_private_key_path or os.getenv(
"CERTIFICATE_PRIVATE_KEY_PATH"
)
certificate_thumbprint = self.options.certificate_thumbprint or os.getenv("CERTIFICATE_THUMBPRINT")
token = self.options.token
managed_identity_client_id = self.options.managed_identity_client_id or os.getenv("MANAGED_IDENTITY_CLIENT_ID")
managed_identity_type = self.options.managed_identity_type or os.getenv("MANAGED_IDENTITY_TYPE") or "user"
Expand Down Expand Up @@ -323,6 +328,16 @@ def _init_credentials(self) -> Optional[Credentials]:
tenant_id=tenant_id,
)

# - If client_id + certificate : use CertificateCredentials (certificate-based auth)
if client_id and certificate_private_key_path and certificate_thumbprint:
with open(certificate_private_key_path, "r") as f:
private_key = f.read()
return CertificateCredentials(
client_id=client_id,
private_key=private_key,
thumbprint=certificate_thumbprint,
tenant_id=tenant_id,
)
return None

@overload
Expand Down
6 changes: 6 additions & 0 deletions packages/apps/src/microsoft/teams/apps/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ class AppOptions(TypedDict, total=False):
token: Optional[Callable[[Union[str, list[str]], Optional[str]], Union[str, Awaitable[str]]]]
"""Custom token provider function. If provided with client_id (no client_secret), uses TokenCredentials."""

# Certificate-based authentication
certificate_private_key_path: Optional[str]
certificate_thumbprint: Optional[str]

# Managed identity configuration (used when client_id provided without client_secret or token)
managed_identity_client_id: Optional[str]
"""
Expand Down Expand Up @@ -66,6 +70,8 @@ class InternalAppOptions:
tenant_id: Optional[str] = None
"""The tenant ID. Required for single-tenant apps."""
token: Optional[Callable[[Union[str, list[str]], Optional[str]], Union[str, Awaitable[str]]]] = None
certificate_private_key_path: Optional[str] = None
certificate_thumbprint: Optional[str] = None
"""Custom token provider function. If provided with client_id (no client_secret), uses TokenCredentials."""
managed_identity_client_id: Optional[str] = None
"""The managed identity client ID for user-assigned managed identity. Defaults to client_id if not provided."""
Expand Down
17 changes: 15 additions & 2 deletions packages/apps/src/microsoft/teams/apps/token_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
JsonWebToken,
TokenProtocol,
)
from microsoft.teams.api.auth.credentials import ManagedIdentityCredentials, TokenCredentials
from microsoft.teams.api.auth.credentials import (
CertificateCredentials,
ManagedIdentityCredentials,
TokenCredentials,
)
from microsoft.teams.common import ConsoleLogger
from msal import ( # pyright: ignore[reportMissingTypeStubs]
ConfidentialClientApplication,
Expand Down Expand Up @@ -69,7 +73,7 @@ async def _get_token(
if caller_name:
self._logger.debug(f"No credentials provided for {caller_name}")
return None
if isinstance(credentials, (ClientCredentials, ManagedIdentityCredentials)):
if isinstance(credentials, (ClientCredentials, CertificateCredentials, ManagedIdentityCredentials)):
tenant_id_param = tenant_id or credentials.tenant_id or "botframework.com"
msal_client = self._get_msal_client(tenant_id_param)

Expand Down Expand Up @@ -123,6 +127,15 @@ def _get_msal_client(self, tenant_id: str) -> ConfidentialClientApplication | Ma
client_credential=credentials.client_secret,
authority=f"https://login.microsoftonline.com/{tenant_id}",
)
elif isinstance(credentials, CertificateCredentials):
client = ConfidentialClientApplication(
credentials.client_id,
client_credential={
"private_key": credentials.private_key,
"thumbprint": credentials.thumbprint,
},
authority=f"https://login.microsoftonline.com/{tenant_id}",
)
elif isinstance(credentials, ManagedIdentityCredentials):
# Create the appropriate managed identity based on type
if credentials.managed_identity_type == "system":
Expand Down
7 changes: 6 additions & 1 deletion stubs/msal/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ class ConfidentialClientApplication:
"""MSAL Confidential Client Application"""

def __init__(
self, client_id: str, *, client_credential: Optional[str] = None, authority: Optional[str] = None, **kwargs: Any
self,
client_id: str,
*,
client_credential: Optional[str | dict[str, Any]] = None,
authority: Optional[str] = None,
**kwargs: Any,
) -> None: ...
def acquire_token_for_client(
self, scopes: list[str], claims_challenge: Optional[str] = None, **kwargs: Any
Expand Down
Loading