Skip to content

Commit 7e9af5d

Browse files
committed
get_msal_token
1 parent 4f49747 commit 7e9af5d

File tree

5 files changed

+71
-28
lines changed

5 files changed

+71
-28
lines changed

src/azure-cli-core/azure/cli/core/_profile.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -299,26 +299,24 @@ def logout_all(self):
299299
identity.logout_all_users()
300300
identity.logout_all_service_principal()
301301

302-
def get_login_credentials(self, subscription_id=None, aux_subscriptions=None, aux_tenants=None):
302+
def get_login_credentials(self, subscription_id=None, aux_subscriptions=None, aux_tenants=None,
303+
sdk_credential=True):
303304
"""Get a credential compatible with Track 2 SDK."""
304305
if aux_tenants and aux_subscriptions:
305306
raise CLIError("Please specify only one of aux_subscriptions and aux_tenants, not both")
306307

307308
account = self.get_subscription(subscription_id)
308309

309310
managed_identity_type, managed_identity_id = Profile._parse_managed_identity_account(account)
310-
311+
external_credentials = None
311312
if in_cloud_console() and account[_USER_ENTITY].get(_CLOUD_SHELL_ID):
312313
# Cloud Shell
313314
from .auth.msal_credentials import CloudShellCredential
314-
# The credential must be wrapped by CredentialAdaptor so that it can work with SDK.
315-
sdk_cred = CredentialAdaptor(CloudShellCredential())
315+
cred = CloudShellCredential()
316316

317317
elif managed_identity_type:
318318
# managed identity
319-
# The credential must be wrapped by CredentialAdaptor so that it can work with SDK.
320319
cred = ManagedIdentityAuth.credential_factory(managed_identity_type, managed_identity_id)
321-
sdk_cred = CredentialAdaptor(cred)
322320

323321
else:
324322
# user and service principal
@@ -332,13 +330,15 @@ def get_login_credentials(self, subscription_id=None, aux_subscriptions=None, au
332330
if sub[_TENANT_ID] != account[_TENANT_ID]:
333331
external_tenants.append(sub[_TENANT_ID])
334332

335-
credential = self._create_credential(account)
333+
cred = self._create_credential(account)
336334
external_credentials = []
337335
for external_tenant in external_tenants:
338336
external_credentials.append(self._create_credential(account, tenant_id=external_tenant))
339-
sdk_cred = CredentialAdaptor(credential, auxiliary_credentials=external_credentials)
340337

341-
return (sdk_cred,
338+
# Wrapping the credential with CredentialAdaptor makes it compatible with SDK.
339+
cred_result = CredentialAdaptor(cred, auxiliary_credentials=external_credentials) if sdk_credential else cred
340+
341+
return (cred_result,
342342
str(account[_SUBSCRIPTION_ID]),
343343
str(account[_TENANT_ID]))
344344

@@ -401,6 +401,15 @@ def get_raw_token(self, resource=None, scopes=None, subscription=None, tenant=No
401401
None if tenant else str(account[_SUBSCRIPTION_ID]),
402402
str(tenant if tenant else account[_TENANT_ID]))
403403

404+
def get_msal_token(self, scopes, data):
405+
"""Get VM SSH certificate. DO NOT use it for other purposes. To get an access token, use get_raw_token instead.
406+
"""
407+
credential, _, _ = self.get_login_credentials(sdk_credential=False)
408+
from .auth.constants import ACCESS_TOKEN
409+
certificate_string = credential.acquire_token(scopes, data=data)[ACCESS_TOKEN]
410+
# The first value used to be username, but it is no longer used.
411+
return None, certificate_string
412+
404413
def _normalize_properties(self, user, subscriptions, is_service_principal, cert_sn_issuer_auth=None,
405414
assigned_identity_info=None):
406415
consolidated = []

src/azure-cli-core/azure/cli/core/auth/credential_adaptor.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,6 @@ def _prepare_msal_kwargs(options=None):
5757
# Both get_token's kwargs and get_token_info's options are accepted as their schema is the same (at least for now).
5858
msal_kwargs = {}
5959
if options:
60-
# For VM SSH. 'data' support is a CLI-specific extension.
61-
# SDK doesn't support 'data': https://github.com/Azure/azure-sdk-for-python/pull/16397
62-
if 'data' in options:
63-
msal_kwargs['data'] = options['data']
6460
# For CAE
6561
if 'claims' in options:
6662
msal_kwargs['claims_challenge'] = options['claims']

src/azure-cli-core/azure/cli/core/auth/msal_credentials.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,23 @@ def __init__(self, client_id, username, **kwargs):
4343

4444
self._account = accounts[0]
4545

46-
def acquire_token(self, scopes, claims_challenge=None, **kwargs):
46+
def acquire_token(self, scopes, claims_challenge=None, data=None, **kwargs):
4747
# scopes must be a list.
4848
# For acquiring SSH certificate, scopes is ['https://pas.windows.net/CheckMyAccess/Linux/.default']
49+
# data is only used for acquiring VM SSH certificate. DO NOT use it for other purposes.
4950
# kwargs is already sanitized by CredentialAdaptor, so it can be safely passed to MSAL
5051
logger.debug("UserCredential.acquire_token: scopes=%r, claims_challenge=%r, kwargs=%r",
51-
scopes, claims_challenge, kwargs)
52+
scopes, claims_challenge)
5253

5354
if claims_challenge:
5455
logger.warning('Acquiring new access token silently for tenant %s with claims challenge: %s',
5556
self._msal_app.authority.tenant, claims_challenge)
57+
58+
# Only pass data to MSAL if it is set. Passing data=None will cause failure in MSAL:
59+
# AttributeError: 'NoneType' object has no attribute 'get'
60+
if data is not None:
61+
kwargs['data'] = data
62+
5663
result = self._msal_app.acquire_token_silent_with_error(
5764
scopes, self._account, claims_challenge=claims_challenge, **kwargs)
5865

@@ -105,8 +112,12 @@ def __init__(self, client_id, client_credential, **kwargs):
105112
"""
106113
self._msal_app = ConfidentialClientApplication(client_id, client_credential=client_credential, **kwargs)
107114

108-
def acquire_token(self, scopes, **kwargs):
115+
def acquire_token(self, scopes, data=None, **kwargs):
109116
logger.debug("ServicePrincipalCredential.acquire_token: scopes=%r, kwargs=%r", scopes, kwargs)
117+
118+
if data is not None:
119+
kwargs['data'] = data
120+
110121
result = self._msal_app.acquire_token_for_client(scopes, **kwargs)
111122
check_result(result)
112123
return result
@@ -126,8 +137,12 @@ def __init__(self):
126137
# token_cache=...
127138
)
128139

129-
def acquire_token(self, scopes, **kwargs):
140+
def acquire_token(self, scopes, data=None, **kwargs):
130141
logger.debug("CloudShellCredential.acquire_token: scopes=%r, kwargs=%r", scopes, kwargs)
142+
143+
if data is not None:
144+
kwargs['data'] = data
145+
131146
result = self._msal_app.acquire_token_interactive(scopes, prompt="none", **kwargs)
132147
check_result(result, scopes=scopes)
133148
return result
@@ -147,9 +162,13 @@ def __init__(self, client_id=None, resource_id=None, object_id=None):
147162
managed_identity = SystemAssignedManagedIdentity()
148163
self._msal_client = ManagedIdentityClient(managed_identity, http_client=requests.Session())
149164

150-
def acquire_token(self, scopes, **kwargs):
165+
def acquire_token(self, scopes, data=None, **kwargs):
151166
logger.debug("ManagedIdentityCredential.acquire_token: scopes=%r, kwargs=%r", scopes, kwargs)
152167

168+
if data is not None:
169+
from azure.cli.core.azclierror import AuthenticationError
170+
raise AuthenticationError("VM SSH currently doesn't support managed identity.")
171+
153172
from .util import scopes_to_resource
154173
result = self._msal_client.acquire_token_for_client(resource=scopes_to_resource(scopes))
155174
check_result(result)

src/azure-cli-core/azure/cli/core/auth/tests/test_credential_adaptor.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def _now_timestamp_mock():
4444

4545
class TestCredentialAdaptor(unittest.TestCase):
4646

47-
@mock.patch('azure.cli.core.auth.util._now_timestamp', new=_now_timestamp_mock)
47+
@mock.patch('azure.cli.core.auth.util.now_timestamp', new=_now_timestamp_mock)
4848
def test_get_token(self):
4949
msal_cred = MsalCredentialStub()
5050
sdk_cred = CredentialAdaptor(msal_cred)
@@ -56,15 +56,11 @@ def test_get_token(self):
5656
assert access_token.token == MOCK_ACCESS_TOKEN
5757
assert access_token.expires_on == 1630920323
5858

59-
# Note that SDK doesn't support 'data'. This is a CLI-specific extension.
60-
sdk_cred.get_token('https://management.core.windows.net//.default', data=MOCK_DATA)
61-
assert msal_cred.acquire_token_kwargs['data'] == MOCK_DATA
62-
6359
sdk_cred.get_token('https://management.core.windows.net//.default', claims=MOCK_CLAIMS)
6460
assert msal_cred.acquire_token_claims_challenge == MOCK_CLAIMS
6561

6662

67-
@mock.patch('azure.cli.core.auth.util._now_timestamp', new=_now_timestamp_mock)
63+
@mock.patch('azure.cli.core.auth.util.now_timestamp', new=_now_timestamp_mock)
6864
def test_get_token_info(self):
6965
msal_cred = MsalCredentialStub()
7066
sdk_cred = CredentialAdaptor(msal_cred)
@@ -78,10 +74,6 @@ def test_get_token_info(self):
7874

7975
assert msal_cred.acquire_token_scopes == ['https://management.core.windows.net//.default']
8076

81-
# Note that SDK doesn't support 'data'. If 'data' were supported, it should be tested with:
82-
sdk_cred.get_token_info('https://management.core.windows.net//.default', options={'data': MOCK_DATA})
83-
assert msal_cred.acquire_token_kwargs['data'] == MOCK_DATA
84-
8577
sdk_cred.get_token_info('https://management.core.windows.net//.default', options={'claims': MOCK_CLAIMS})
8678
assert msal_cred.acquire_token_claims_challenge == MOCK_CLAIMS
8779

src/azure-cli-core/azure/cli/core/tests/test_profile.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,12 @@ def __init__(self, *args, **kwargs):
5050
# If acquire_token_scopes is checked, make sure to create a new instance of MsalCredentialStub
5151
# to avoid interference from other tests.
5252
self.acquire_token_scopes = None
53+
self.acquire_token_data=None
5354
super().__init__()
5455

5556
def acquire_token(self, scopes, **kwargs):
5657
self.acquire_token_scopes = scopes
58+
self.acquire_token_data = kwargs.get('data')
5759
return {
5860
'access_token': MOCK_ACCESS_TOKEN,
5961
'token_type': 'Bearer',
@@ -1287,6 +1289,31 @@ def cloud_shell_credential_factory():
12871289
with self.assertRaisesRegex(CLIError, 'Cloud Shell'):
12881290
profile.get_raw_token(resource='http://test_resource', tenant=self.tenant_id)
12891291

1292+
@mock.patch('azure.cli.core.auth.identity.Identity.get_user_credential')
1293+
def test_get_msal_token(self, get_user_credential_mock):
1294+
credential_mock_temp = MsalCredentialStub()
1295+
get_user_credential_mock.return_value = credential_mock_temp
1296+
cli = DummyCli()
1297+
1298+
storage_mock = {'subscriptions': None}
1299+
profile = Profile(cli_ctx=cli, storage=storage_mock)
1300+
consolidated = profile._normalize_properties(self.user1,
1301+
[self.subscription1],
1302+
False, None, None)
1303+
profile._set_subscriptions(consolidated)
1304+
1305+
MOCK_DATA = {
1306+
'key_id': 'test',
1307+
'req_cnf': 'test',
1308+
'token_type': 'ssh-cert'
1309+
}
1310+
result = profile.get_msal_token(['https://pas.windows.net/CheckMyAccess/Linux/.default'],
1311+
MOCK_DATA)
1312+
1313+
assert result == (None, MOCK_ACCESS_TOKEN)
1314+
assert credential_mock_temp.acquire_token_scopes == ['https://pas.windows.net/CheckMyAccess/Linux/.default']
1315+
assert credential_mock_temp.acquire_token_data == MOCK_DATA
1316+
12901317
@mock.patch('azure.cli.core.auth.identity.Identity.logout_service_principal')
12911318
@mock.patch('azure.cli.core.auth.identity.Identity.logout_user')
12921319
def test_logout(self, logout_user_mock, logout_service_principal_mock):

0 commit comments

Comments
 (0)