Skip to content

Commit 9f37215

Browse files
committed
Add support for certificate files
1 parent d68fbdc commit 9f37215

File tree

6 files changed

+72
-5
lines changed

6 files changed

+72
-5
lines changed

packages/api/src/microsoft/teams/api/auth/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,20 @@
44
"""
55

66
from .caller import CallerIds, CallerType
7-
from .credentials import ClientCredentials, Credentials, ManagedIdentityCredentials, TokenCredentials
7+
from .credentials import (
8+
CertificateCredentials,
9+
ClientCredentials,
10+
Credentials,
11+
ManagedIdentityCredentials,
12+
TokenCredentials,
13+
)
814
from .json_web_token import JsonWebToken, JsonWebTokenPayload
915
from .token import TokenProtocol
1016

1117
__all__ = [
1218
"CallerIds",
1319
"CallerType",
20+
"CertificateCredentials",
1421
"ClientCredentials",
1522
"Credentials",
1623
"ManagedIdentityCredentials",

packages/api/src/microsoft/teams/api/auth/credentials.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,27 @@ class TokenCredentials(CustomBaseModel):
4343
"""
4444

4545

46+
class CertificateCredentials(CustomBaseModel):
47+
"""Credentials for authentication using X.509 certificate (PEM format)."""
48+
49+
client_id: str
50+
"""
51+
The client ID.
52+
"""
53+
private_key: str
54+
"""
55+
The private key in PEM format.
56+
"""
57+
thumbprint: str
58+
"""
59+
The SHA-1 thumbprint of the certificate.
60+
"""
61+
tenant_id: Optional[str] = None
62+
"""
63+
The tenant ID. This should only be passed in for single tenant apps.
64+
"""
65+
66+
4667
class ManagedIdentityCredentials(CustomBaseModel):
4768
"""Credentials for authentication using Azure Managed Identity."""
4869

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

6283

6384
# Union type for credentials
64-
Credentials = Union[ClientCredentials, TokenCredentials, ManagedIdentityCredentials]
85+
Credentials = Union[ClientCredentials, TokenCredentials, CertificateCredentials, ManagedIdentityCredentials]

packages/apps/src/microsoft/teams/apps/app.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
ActivityBase,
1818
ActivityParams,
1919
ApiClient,
20+
CertificateCredentials,
2021
ClientCredentials,
2122
ConversationAccount,
2223
ConversationReference,
@@ -290,6 +291,10 @@ def _init_credentials(self) -> Optional[Credentials]:
290291
client_id = self.options.client_id or os.getenv("CLIENT_ID")
291292
client_secret = self.options.client_secret or os.getenv("CLIENT_SECRET")
292293
tenant_id = self.options.tenant_id or os.getenv("TENANT_ID")
294+
certificate_private_key_path = self.options.certificate_private_key_path or os.getenv(
295+
"CERTIFICATE_PRIVATE_KEY_PATH"
296+
)
297+
certificate_thumbprint = self.options.certificate_thumbprint or os.getenv("CERTIFICATE_THUMBPRINT")
293298
token = self.options.token
294299
managed_identity_client_id = self.options.managed_identity_client_id or os.getenv("MANAGED_IDENTITY_CLIENT_ID")
295300
managed_identity_type = self.options.managed_identity_type or os.getenv("MANAGED_IDENTITY_TYPE") or "user"
@@ -322,6 +327,16 @@ def _init_credentials(self) -> Optional[Credentials]:
322327
tenant_id=tenant_id,
323328
)
324329

330+
# - If client_id + certificate : use CertificateCredentials (certificate-based auth)
331+
if client_id and certificate_private_key_path and certificate_thumbprint:
332+
with open(certificate_private_key_path, "r") as f:
333+
private_key = f.read()
334+
return CertificateCredentials(
335+
client_id=client_id,
336+
private_key=private_key,
337+
thumbprint=certificate_thumbprint,
338+
tenant_id=tenant_id,
339+
)
325340
return None
326341

327342
@overload

packages/apps/src/microsoft/teams/apps/options.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ class AppOptions(TypedDict, total=False):
2626
token: Optional[Callable[[Union[str, list[str]], Optional[str]], Union[str, Awaitable[str]]]]
2727
"""Custom token provider function. If provided with client_id (no client_secret), uses TokenCredentials."""
2828

29+
# Certificate-based authentication
30+
certificate_private_key_path: Optional[str]
31+
certificate_thumbprint: Optional[str]
32+
2933
# Managed identity configuration (used when client_id provided without client_secret or token)
3034
managed_identity_client_id: Optional[str]
3135
"""
@@ -66,6 +70,8 @@ class InternalAppOptions:
6670
tenant_id: Optional[str] = None
6771
"""The tenant ID. Required for single-tenant apps."""
6872
token: Optional[Callable[[Union[str, list[str]], Optional[str]], Union[str, Awaitable[str]]]] = None
73+
certificate_private_key_path: Optional[str] = None
74+
certificate_thumbprint: Optional[str] = None
6975
"""Custom token provider function. If provided with client_id (no client_secret), uses TokenCredentials."""
7076
managed_identity_client_id: Optional[str] = None
7177
"""The managed identity client ID for user-assigned managed identity. Defaults to client_id if not provided."""

packages/apps/src/microsoft/teams/apps/token_manager.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
JsonWebToken,
1616
TokenProtocol,
1717
)
18-
from microsoft.teams.api.auth.credentials import ManagedIdentityCredentials, TokenCredentials
18+
from microsoft.teams.api.auth.credentials import (
19+
CertificateCredentials,
20+
ManagedIdentityCredentials,
21+
TokenCredentials,
22+
)
1923
from microsoft.teams.common import ConsoleLogger
2024
from msal import ( # pyright: ignore[reportMissingTypeStubs]
2125
ConfidentialClientApplication,
@@ -69,7 +73,7 @@ async def _get_token(
6973
if caller_name:
7074
self._logger.debug(f"No credentials provided for {caller_name}")
7175
return None
72-
if isinstance(credentials, (ClientCredentials, ManagedIdentityCredentials)):
76+
if isinstance(credentials, (ClientCredentials, CertificateCredentials, ManagedIdentityCredentials)):
7377
tenant_id_param = tenant_id or credentials.tenant_id or "botframework.com"
7478
msal_client = self._get_msal_client(tenant_id_param)
7579

@@ -123,6 +127,15 @@ def _get_msal_client(self, tenant_id: str) -> ConfidentialClientApplication | Ma
123127
client_credential=credentials.client_secret,
124128
authority=f"https://login.microsoftonline.com/{tenant_id}",
125129
)
130+
elif isinstance(credentials, CertificateCredentials):
131+
client = ConfidentialClientApplication(
132+
credentials.client_id,
133+
client_credential={
134+
"private_key": credentials.private_key,
135+
"thumbprint": credentials.thumbprint,
136+
},
137+
authority=f"https://login.microsoftonline.com/{tenant_id}",
138+
)
126139
elif isinstance(credentials, ManagedIdentityCredentials):
127140
# Create the appropriate managed identity based on type
128141
if credentials.managed_identity_type == "system":

stubs/msal/__init__.pyi

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ class ConfidentialClientApplication:
66
"""MSAL Confidential Client Application"""
77

88
def __init__(
9-
self, client_id: str, *, client_credential: Optional[str] = None, authority: Optional[str] = None, **kwargs: Any
9+
self,
10+
client_id: str,
11+
*,
12+
client_credential: Optional[str | dict[str, Any]] = None,
13+
authority: Optional[str] = None,
14+
**kwargs: Any,
1015
) -> None: ...
1116
def acquire_token_for_client(
1217
self, scopes: list[str], claims_challenge: Optional[str] = None, **kwargs: Any

0 commit comments

Comments
 (0)