Skip to content

Commit acfa775

Browse files
tracyboehrerTracy Boehrer
and
Tracy Boehrer
authored
Added CertificateServiceClientCredentialsFactory (#2132)
* Added CertificateServiceClientCredentialsFactory * Added Gov and Private cloud support to CertificateServiceClientCredentialsFactory * Corrected _CertificatePrivateCloudAppCredentials * Fixed _CertificatePrivateCloudAppCredentials creation * Added CertificateServiceClientCredentialsFactoryTests * CertificateServiceClientCredentialsFactoryTests formatting * Corrected CertificateAppCredentials scopes * CertificateAppCredentials formatting --------- Co-authored-by: Tracy Boehrer <trboehre@microsoft.com>
1 parent d7b20cb commit acfa775

5 files changed

+262
-7
lines changed

libraries/botframework-connector/botframework/connector/auth/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
from .microsoft_app_credentials import *
1818
from .microsoft_government_app_credentials import *
1919
from .certificate_app_credentials import *
20+
from .certificate_government_app_credentials import *
21+
from .certificate_service_client_credential_factory import *
2022
from .claims_identity import *
2123
from .jwt_token_validation import *
2224
from .credential_provider import *

libraries/botframework-connector/botframework/connector/auth/certificate_app_credentials.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ def __init__(
4444
oauth_scope=oauth_scope,
4545
)
4646

47-
self.scopes = [self.oauth_scope]
4847
self.app = None
4948
self.certificate_thumbprint = certificate_thumbprint
5049
self.certificate_private_key = certificate_private_key
@@ -56,17 +55,18 @@ def get_access_token(self, force_refresh: bool = False) -> str:
5655
:return: The access token for the given certificate.
5756
"""
5857

58+
scope = self.oauth_scope
59+
if not scope.endswith("/.default"):
60+
scope += "/.default"
61+
scopes = [scope]
62+
5963
# Firstly, looks up a token from cache
6064
# Since we are looking for token for the current app, NOT for an end user,
6165
# notice we give account parameter as None.
62-
auth_token = self.__get_msal_app().acquire_token_silent(
63-
self.scopes, account=None
64-
)
66+
auth_token = self.__get_msal_app().acquire_token_silent(scopes, account=None)
6567
if not auth_token:
6668
# No suitable token exists in cache. Let's get a new one from AAD.
67-
auth_token = self.__get_msal_app().acquire_token_for_client(
68-
scopes=self.scopes
69-
)
69+
auth_token = self.__get_msal_app().acquire_token_for_client(scopes=scopes)
7070
return auth_token["access_token"]
7171

7272
def __get_msal_app(self):
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from .certificate_app_credentials import CertificateAppCredentials
5+
from .government_constants import GovernmentConstants
6+
7+
8+
class CertificateGovernmentAppCredentials(CertificateAppCredentials):
9+
"""
10+
GovernmentAppCredentials implementation using a certificate.
11+
"""
12+
13+
def __init__(
14+
self,
15+
app_id: str,
16+
certificate_thumbprint: str,
17+
certificate_private_key: str,
18+
channel_auth_tenant: str = None,
19+
oauth_scope: str = None,
20+
certificate_public: str = None,
21+
):
22+
"""
23+
AppCredentials implementation using a certificate.
24+
25+
:param app_id:
26+
:param certificate_thumbprint:
27+
:param certificate_private_key:
28+
:param channel_auth_tenant:
29+
:param oauth_scope:
30+
:param certificate_public: public_certificate (optional) is public key certificate which will be sent
31+
through ‘x5c’ JWT header only for subject name and issuer authentication to support cert auto rolls.
32+
"""
33+
34+
# super will set proper scope and endpoint.
35+
super().__init__(
36+
app_id=app_id,
37+
channel_auth_tenant=channel_auth_tenant,
38+
oauth_scope=oauth_scope,
39+
certificate_thumbprint=certificate_thumbprint,
40+
certificate_private_key=certificate_private_key,
41+
certificate_public=certificate_public,
42+
)
43+
44+
def _get_default_channelauth_tenant(self) -> str:
45+
return GovernmentConstants.DEFAULT_CHANNEL_AUTH_TENANT
46+
47+
def _get_to_channel_from_bot_loginurl_prefix(self) -> str:
48+
return GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX
49+
50+
def _get_to_channel_from_bot_oauthscope(self) -> str:
51+
return GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from logging import Logger
5+
6+
from msrest.authentication import Authentication
7+
8+
from .authentication_constants import AuthenticationConstants
9+
from .government_constants import GovernmentConstants
10+
from .certificate_app_credentials import CertificateAppCredentials
11+
from .certificate_government_app_credentials import CertificateGovernmentAppCredentials
12+
from .microsoft_app_credentials import MicrosoftAppCredentials
13+
from .service_client_credentials_factory import ServiceClientCredentialsFactory
14+
15+
16+
class CertificateServiceClientCredentialsFactory(ServiceClientCredentialsFactory):
17+
def __init__(
18+
self,
19+
certificate_thumbprint: str,
20+
certificate_private_key: str,
21+
app_id: str,
22+
tenant_id: str = None,
23+
certificate_public: str = None,
24+
*,
25+
logger: Logger = None
26+
) -> None:
27+
"""
28+
CertificateServiceClientCredentialsFactory implementation using a certificate.
29+
30+
:param certificate_thumbprint:
31+
:param certificate_private_key:
32+
:param app_id:
33+
:param tenant_id:
34+
:param certificate_public: public_certificate (optional) is public key certificate which will be sent
35+
through ‘x5c’ JWT header only for subject name and issuer authentication to support cert auto rolls.
36+
"""
37+
38+
self.certificate_thumbprint = certificate_thumbprint
39+
self.certificate_private_key = certificate_private_key
40+
self.app_id = app_id
41+
self.tenant_id = tenant_id
42+
self.certificate_public = certificate_public
43+
self._logger = logger
44+
45+
async def is_valid_app_id(self, app_id: str) -> bool:
46+
return app_id == self.app_id
47+
48+
async def is_authentication_disabled(self) -> bool:
49+
return not self.app_id
50+
51+
async def create_credentials(
52+
self,
53+
app_id: str,
54+
oauth_scope: str,
55+
login_endpoint: str,
56+
validate_authority: bool,
57+
) -> Authentication:
58+
if await self.is_authentication_disabled():
59+
return MicrosoftAppCredentials.empty()
60+
61+
if not await self.is_valid_app_id(app_id):
62+
raise Exception("Invalid app_id")
63+
64+
normalized_endpoint = login_endpoint.lower() if login_endpoint else ""
65+
66+
if normalized_endpoint.startswith(
67+
AuthenticationConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX
68+
):
69+
credentials = CertificateAppCredentials(
70+
app_id,
71+
self.certificate_thumbprint,
72+
self.certificate_private_key,
73+
self.tenant_id,
74+
oauth_scope,
75+
self.certificate_public,
76+
)
77+
elif normalized_endpoint.startswith(
78+
GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX
79+
):
80+
credentials = CertificateGovernmentAppCredentials(
81+
app_id,
82+
self.certificate_thumbprint,
83+
self.certificate_private_key,
84+
self.tenant_id,
85+
oauth_scope,
86+
self.certificate_public,
87+
)
88+
else:
89+
credentials = _CertificatePrivateCloudAppCredentials(
90+
app_id,
91+
self.certificate_thumbprint,
92+
self.certificate_private_key,
93+
self.tenant_id,
94+
oauth_scope,
95+
self.certificate_public,
96+
login_endpoint,
97+
validate_authority,
98+
)
99+
100+
return credentials
101+
102+
103+
class _CertificatePrivateCloudAppCredentials(CertificateAppCredentials):
104+
def __init__(
105+
self,
106+
app_id: str,
107+
certificate_thumbprint: str,
108+
certificate_private_key: str,
109+
channel_auth_tenant: str,
110+
oauth_scope: str,
111+
certificate_public: str,
112+
oauth_endpoint: str,
113+
validate_authority: bool,
114+
):
115+
super().__init__(
116+
app_id,
117+
certificate_thumbprint,
118+
certificate_private_key,
119+
channel_auth_tenant,
120+
oauth_scope,
121+
certificate_public,
122+
)
123+
124+
self.oauth_endpoint = oauth_endpoint
125+
self._validate_authority = validate_authority
126+
127+
@property
128+
def validate_authority(self):
129+
return self._validate_authority
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import aiounittest
5+
from botframework.connector.auth import (
6+
AppCredentials,
7+
AuthenticationConstants,
8+
GovernmentConstants,
9+
CertificateServiceClientCredentialsFactory,
10+
CertificateAppCredentials,
11+
CertificateGovernmentAppCredentials,
12+
)
13+
14+
15+
class CertificateServiceClientCredentialsFactoryTests(aiounittest.AsyncTestCase):
16+
test_appid = "test_appid"
17+
test_tenant_id = "test_tenant_id"
18+
test_audience = "test_audience"
19+
login_endpoint = AuthenticationConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX
20+
gov_login_endpoint = GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX
21+
private_login_endpoint = "https://login.privatecloud.com"
22+
23+
async def test_can_create_public_credentials(self):
24+
factory = CertificateServiceClientCredentialsFactory(
25+
app_id=CertificateServiceClientCredentialsFactoryTests.test_appid,
26+
certificate_thumbprint="thumbprint",
27+
certificate_private_key="private_key",
28+
)
29+
30+
credentials = await factory.create_credentials(
31+
CertificateServiceClientCredentialsFactoryTests.test_appid,
32+
CertificateServiceClientCredentialsFactoryTests.test_audience,
33+
CertificateServiceClientCredentialsFactoryTests.login_endpoint,
34+
True,
35+
)
36+
37+
assert isinstance(credentials, CertificateAppCredentials)
38+
39+
async def test_can_create_gov_credentials(self):
40+
factory = CertificateServiceClientCredentialsFactory(
41+
app_id=CertificateServiceClientCredentialsFactoryTests.test_appid,
42+
certificate_thumbprint="thumbprint",
43+
certificate_private_key="private_key",
44+
)
45+
46+
credentials = await factory.create_credentials(
47+
CertificateServiceClientCredentialsFactoryTests.test_appid,
48+
CertificateServiceClientCredentialsFactoryTests.test_audience,
49+
CertificateServiceClientCredentialsFactoryTests.gov_login_endpoint,
50+
True,
51+
)
52+
53+
assert isinstance(credentials, CertificateGovernmentAppCredentials)
54+
55+
async def test_can_create_private_credentials(self):
56+
factory = CertificateServiceClientCredentialsFactory(
57+
app_id=CertificateServiceClientCredentialsFactoryTests.test_appid,
58+
certificate_thumbprint="thumbprint",
59+
certificate_private_key="private_key",
60+
)
61+
62+
credentials = await factory.create_credentials(
63+
CertificateServiceClientCredentialsFactoryTests.test_appid,
64+
CertificateServiceClientCredentialsFactoryTests.test_audience,
65+
CertificateServiceClientCredentialsFactoryTests.private_login_endpoint,
66+
True,
67+
)
68+
69+
assert isinstance(credentials, CertificateAppCredentials)
70+
assert (
71+
credentials.oauth_endpoint
72+
== CertificateServiceClientCredentialsFactoryTests.private_login_endpoint
73+
)

0 commit comments

Comments
 (0)