Skip to content

Commit fa3cfca

Browse files
committed
Add auth files
1 parent 29d61a8 commit fa3cfca

File tree

12 files changed

+347
-13
lines changed

12 files changed

+347
-13
lines changed

packages/api/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ requires-python = ">=3.12"
1313
dependencies = [
1414
"pydantic>=2.0.0",
1515
"microsoft-teams-common",
16+
"jwt>=1.3.1",
1617
]
1718

1819
[tool.uv.sources]

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
Licensed under the MIT License.
44
"""
55

6+
from .auth import * # noqa: F403
7+
from .auth import __all__ as auth_all
68
from .clients import * # noqa: F403
79
from .clients import __all__ as clients_all
810
from .models import * # noqa: F403
911
from .models import __all__ as models_all
1012

1113
# Combine all exports from submodules
1214
__all__ = [
15+
*auth_all,
1316
*clients_all,
1417
*models_all,
1518
]
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""
2+
Copyright (c) Microsoft Corporation. All rights reserved.
3+
Licensed under the MIT License.
4+
"""
5+
6+
from .caller import CALLER_IDS, CallerType
7+
from .credentials import ClientCredentials, Credentials, TokenCredentials
8+
from .json_web_token import JsonWebToken, JsonWebTokenPayload
9+
from .token import IToken
10+
11+
__all__ = [
12+
"CALLER_IDS",
13+
"CallerType",
14+
"ClientCredentials",
15+
"Credentials",
16+
"TokenCredentials",
17+
"IToken",
18+
"JsonWebToken",
19+
"JsonWebTokenPayload",
20+
]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""
2+
Copyright (c) Microsoft Corporation. All rights reserved.
3+
Licensed under the MIT License.
4+
"""
5+
6+
from typing import Literal
7+
8+
CallerType = Literal["azure", "gov", "bot"]
9+
10+
CALLER_IDS = {
11+
"azure": "urn:botframework:azure",
12+
"gov": "urn:botframework:azureusgov",
13+
"bot": "urn:botframework:aadappid",
14+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
"""
2+
Copyright (c) Microsoft Corporation. All rights reserved.
3+
Licensed under the MIT License.
4+
"""
5+
6+
import time
7+
from typing import Optional, Union
8+
9+
import jwt
10+
from pydantic import BaseModel, ConfigDict
11+
12+
from .caller import CALLER_IDS, CallerType
13+
from .token import IToken
14+
15+
16+
class JsonWebTokenPayload(BaseModel):
17+
"""JWT payload with additional Teams-specific fields."""
18+
19+
model_config = ConfigDict(extra="allow")
20+
21+
aud: Optional[Union[str, list[str]]] = None
22+
iss: Optional[str] = None
23+
exp: Optional[int] = None
24+
kid: Optional[str] = None
25+
appid: Optional[str] = None
26+
app_displayname: Optional[str] = None
27+
tid: Optional[str] = None
28+
version: Optional[str] = None
29+
serviceurl: Optional[str] = None
30+
31+
32+
class JsonWebToken(IToken):
33+
"""JSON Web Token implementation for Teams authentication."""
34+
35+
def __init__(self, value: str):
36+
"""
37+
Initialize JWT from token string.
38+
39+
Args:
40+
value: The JWT token string.
41+
"""
42+
self._value = value
43+
# Decode without verification for payload extraction
44+
jwt_instance = jwt.JWT()
45+
self._payload = JsonWebTokenPayload(**jwt_instance.decode(value, do_verify=False, do_time_check=False))
46+
47+
@property
48+
def audience(self) -> Optional[Union[str, list[str]]]:
49+
"""The token audience."""
50+
return self._payload.aud
51+
52+
@property
53+
def issuer(self) -> Optional[str]:
54+
"""The token issuer."""
55+
return self._payload.iss
56+
57+
@property
58+
def key_id(self) -> Optional[str]:
59+
"""The key ID."""
60+
return self._payload.kid
61+
62+
@property
63+
def app_id(self) -> str:
64+
"""The app ID."""
65+
return self._payload.appid or ""
66+
67+
@property
68+
def app_display_name(self) -> Optional[str]:
69+
"""The app display name."""
70+
return self._payload.app_displayname
71+
72+
@property
73+
def tenant_id(self) -> Optional[str]:
74+
"""The tenant ID."""
75+
return self._payload.tid
76+
77+
@property
78+
def version(self) -> Optional[str]:
79+
"""The token version."""
80+
return self._payload.version
81+
82+
@property
83+
def service_url(self) -> str:
84+
"""The service URL to send responses to."""
85+
url = self._payload.serviceurl or "https://smba.trafficmanager.net/teams"
86+
87+
if url.endswith("/"):
88+
url = url[:-1]
89+
90+
return url
91+
92+
@property
93+
def from_(self) -> CallerType:
94+
"""Where the activity originated from."""
95+
if self.app_id:
96+
return "bot"
97+
return "azure"
98+
99+
@property
100+
def from_id(self) -> str:
101+
"""The id of the activity sender."""
102+
if self.from_ == "bot":
103+
return f"{CALLER_IDS['bot']}:{self.app_id}"
104+
return CALLER_IDS["azure"]
105+
106+
@property
107+
def expiration(self) -> Optional[int]:
108+
"""The expiration of the token since epoch in milliseconds."""
109+
if self._payload.exp:
110+
return self._payload.exp * 1000
111+
return None
112+
113+
def is_expired(self, buffer_ms: int = 5 * 60 * 1000) -> bool:
114+
"""
115+
Check if the token is expired.
116+
117+
Args:
118+
buffer_ms: Buffer time in milliseconds (default 5 minutes).
119+
120+
Returns:
121+
True if the token is expired, False otherwise.
122+
"""
123+
if not self.expiration:
124+
return False
125+
return self.expiration < (time.time() * 1000) + buffer_ms
126+
127+
def __str__(self) -> str:
128+
"""String form of the token."""
129+
return self._value
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""
2+
Copyright (c) Microsoft Corporation. All rights reserved.
3+
Licensed under the MIT License.
4+
"""
5+
6+
from abc import ABC, abstractmethod
7+
from typing import Optional
8+
9+
from .caller import CallerType
10+
11+
12+
class IToken(ABC):
13+
"""Any authorized token."""
14+
15+
@property
16+
@abstractmethod
17+
def app_id(self) -> str:
18+
"""The app id."""
19+
pass
20+
21+
@property
22+
@abstractmethod
23+
def app_display_name(self) -> Optional[str]:
24+
"""The app display name."""
25+
pass
26+
27+
@property
28+
@abstractmethod
29+
def tenant_id(self) -> Optional[str]:
30+
"""The tenant id."""
31+
pass
32+
33+
@property
34+
@abstractmethod
35+
def service_url(self) -> str:
36+
"""The service url to send responses to."""
37+
pass
38+
39+
@property
40+
@abstractmethod
41+
def from_(self) -> CallerType:
42+
"""Where the activity originated from."""
43+
pass
44+
45+
@property
46+
@abstractmethod
47+
def from_id(self) -> str:
48+
"""The id of the activity sender."""
49+
pass
50+
51+
@property
52+
@abstractmethod
53+
def expiration(self) -> Optional[int]:
54+
"""The expiration of the token since epoch in milliseconds."""
55+
pass
56+
57+
@abstractmethod
58+
def is_expired(self, buffer_ms: int = 5 * 60 * 1000) -> bool:
59+
"""
60+
Check if the token is expired.
61+
62+
Args:
63+
buffer_ms: Buffer time in milliseconds (default 5 minutes).
64+
65+
Returns:
66+
True if the token is expired, False otherwise.
67+
"""
68+
pass
69+
70+
@abstractmethod
71+
def __str__(self) -> str:
72+
"""String form of the token."""
73+
pass

packages/api/src/microsoft/teams/api/clients/bot/token_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from pydantic import AliasGenerator, BaseModel, ConfigDict
1111
from pydantic.alias_generators import to_camel
1212

13-
from ...models import Credentials, TokenCredentials
13+
from ...auth import Credentials, TokenCredentials
1414
from ..base_client import BaseClient
1515

1616

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55

66
from .account import Account, AccountRole
77
from .activity import Activity
8-
from .auth import * # noqa: F403
9-
from .auth import __all__ as auth_all
108
from .channel_id import ChannelID
119
from .conversation import * # noqa: F403
1210
from .conversation import __all__ as conversation_all
@@ -25,6 +23,5 @@
2523
*conversation_all,
2624
*sign_in_all,
2725
*token_all,
28-
*auth_all,
2926
*token_exchange_all,
3027
]

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

Lines changed: 0 additions & 8 deletions
This file was deleted.

0 commit comments

Comments
 (0)