Skip to content

Commit 29a241f

Browse files
authored
{Auth} Support get_token_info protocol (#30928)
1 parent 7ee3544 commit 29a241f

File tree

4 files changed

+145
-15
lines changed

4 files changed

+145
-15
lines changed

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

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,24 @@ def __init__(self, credential, auxiliary_credentials=None):
2424
self._auxiliary_credentials = auxiliary_credentials
2525

2626
def get_token(self, *scopes, **kwargs):
27-
"""Get an access token from the main credential."""
27+
"""Implement the old SDK token protocol azure.core.credentials.TokenCredential
28+
Return azure.core.credentials.AccessToken
29+
"""
2830
logger.debug("CredentialAdaptor.get_token: scopes=%r, kwargs=%r", scopes, kwargs)
2931

30-
# Discard unsupported kwargs: tenant_id, enable_cae
31-
filtered_kwargs = {}
32-
if 'data' in kwargs:
33-
filtered_kwargs['data'] = kwargs['data']
32+
msal_kwargs = _prepare_msal_kwargs(kwargs)
33+
msal_result = self._credential.acquire_token(list(scopes), **msal_kwargs)
34+
return build_sdk_access_token(msal_result)
35+
36+
def get_token_info(self, *scopes, options=None):
37+
"""Implement the new SDK token protocol azure.core.credentials.SupportsTokenInfo
38+
Return azure.core.credentials.AccessTokenInfo
39+
"""
40+
logger.debug("CredentialAdaptor.get_token_info: scopes=%r, options=%r", scopes, options)
3441

35-
return build_sdk_access_token(self._credential.acquire_token(list(scopes), **filtered_kwargs))
42+
msal_kwargs = _prepare_msal_kwargs(options)
43+
msal_result = self._credential.acquire_token(list(scopes), **msal_kwargs)
44+
return _build_sdk_access_token_info(msal_result)
3645

3746
def get_auxiliary_tokens(self, *scopes, **kwargs):
3847
"""Get access tokens from auxiliary credentials."""
@@ -41,3 +50,33 @@ def get_auxiliary_tokens(self, *scopes, **kwargs):
4150
return [build_sdk_access_token(cred.acquire_token(list(scopes), **kwargs))
4251
for cred in self._auxiliary_credentials]
4352
return None
53+
54+
55+
def _prepare_msal_kwargs(options=None):
56+
# Preserve supported options and discard unsupported options (tenant_id, enable_cae).
57+
# Both get_token's kwargs and get_token_info's options are accepted as their schema is the same (at least for now).
58+
msal_kwargs = {}
59+
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']
64+
# For CAE
65+
if 'claims' in options:
66+
msal_kwargs['claims_challenge'] = options['claims']
67+
return msal_kwargs
68+
69+
70+
def _build_sdk_access_token_info(token_entry):
71+
# MSAL token entry sample:
72+
# {
73+
# 'access_token': 'eyJ0eXAiOiJKV...',
74+
# 'token_type': 'Bearer',
75+
# 'expires_in': 1618,
76+
# 'token_source': 'cache'
77+
# }
78+
from .constants import ACCESS_TOKEN, EXPIRES_IN
79+
from .util import _now_timestamp
80+
from azure.core.credentials import AccessTokenInfo
81+
82+
return AccessTokenInfo(token_entry[ACCESS_TOKEN], _now_timestamp() + token_entry[EXPIRES_IN])

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

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

4444
self._account = accounts[0]
4545

46-
def acquire_token(self, scopes, claims=None, **kwargs):
46+
def acquire_token(self, scopes, claims_challenge=None, **kwargs):
4747
# scopes must be a list.
4848
# For acquiring SSH certificate, scopes is ['https://pas.windows.net/CheckMyAccess/Linux/.default']
4949
# kwargs is already sanitized by CredentialAdaptor, so it can be safely passed to MSAL
50-
logger.debug("UserCredential.acquire_token: scopes=%r, claims=%r, kwargs=%r", scopes, claims, kwargs)
50+
logger.debug("UserCredential.acquire_token: scopes=%r, claims_challenge=%r, kwargs=%r",
51+
scopes, claims_challenge, kwargs)
5152

52-
if claims:
53+
if claims_challenge:
5354
logger.warning('Acquiring new access token silently for tenant %s with claims challenge: %s',
54-
self._msal_app.authority.tenant, claims)
55-
result = self._msal_app.acquire_token_silent_with_error(scopes, self._account, claims_challenge=claims,
56-
**kwargs)
55+
self._msal_app.authority.tenant, claims_challenge)
56+
result = self._msal_app.acquire_token_silent_with_error(
57+
scopes, self._account, claims_challenge=claims_challenge, **kwargs)
5758

5859
from azure.cli.core.azclierror import AuthenticationError
5960
try:
6061
# Check if an access token is returned.
61-
check_result(result, scopes=scopes, claims=claims)
62+
check_result(result, scopes=scopes, claims_challenge=claims_challenge)
6263
except AuthenticationError as ex:
6364
# For VM SSH ('data' is passed), if getting access token fails because
6465
# Conditional Access MFA step-up or compliance check is required, re-launch
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
7+
import unittest
8+
from unittest import mock
9+
10+
from ..credential_adaptor import CredentialAdaptor
11+
12+
13+
MOCK_ACCESS_TOKEN = "mock_access_token"
14+
MOCK_DATA = {
15+
'key_id': 'test',
16+
'req_cnf': 'test',
17+
'token_type': 'ssh-cert'
18+
}
19+
MOCK_CLAIMS = {"test_claims": "value2"}
20+
21+
class MsalCredentialStub:
22+
23+
def __init__(self, *args, **kwargs):
24+
self.acquire_token_scopes = None
25+
self.acquire_token_claims_challenge = None
26+
self.acquire_token_kwargs = None
27+
super().__init__()
28+
29+
def acquire_token(self, scopes, claims_challenge=None, **kwargs):
30+
self.acquire_token_scopes = scopes
31+
self.acquire_token_claims_challenge = claims_challenge
32+
self.acquire_token_kwargs = kwargs
33+
return {
34+
'access_token': MOCK_ACCESS_TOKEN,
35+
'token_type': 'Bearer',
36+
'expires_in': 1800,
37+
'token_source': 'cache'
38+
}
39+
40+
def _now_timestamp_mock():
41+
# 2021-09-06 08:55:23
42+
return 1630918523
43+
44+
45+
class TestCredentialAdaptor(unittest.TestCase):
46+
47+
@mock.patch('azure.cli.core.auth.util._now_timestamp', new=_now_timestamp_mock)
48+
def test_get_token(self):
49+
msal_cred = MsalCredentialStub()
50+
sdk_cred = CredentialAdaptor(msal_cred)
51+
access_token = sdk_cred.get_token('https://management.core.windows.net//.default')
52+
assert msal_cred.acquire_token_scopes == ['https://management.core.windows.net//.default']
53+
54+
from ..util import AccessToken
55+
assert isinstance(access_token, AccessToken)
56+
assert access_token.token == MOCK_ACCESS_TOKEN
57+
assert access_token.expires_on == 1630920323
58+
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+
63+
sdk_cred.get_token('https://management.core.windows.net//.default', claims=MOCK_CLAIMS)
64+
assert msal_cred.acquire_token_claims_challenge == MOCK_CLAIMS
65+
66+
67+
@mock.patch('azure.cli.core.auth.util._now_timestamp', new=_now_timestamp_mock)
68+
def test_get_token_info(self):
69+
msal_cred = MsalCredentialStub()
70+
sdk_cred = CredentialAdaptor(msal_cred)
71+
access_token_info = sdk_cred.get_token_info('https://management.core.windows.net//.default')
72+
73+
from azure.core.credentials import AccessTokenInfo
74+
assert isinstance(access_token_info, AccessTokenInfo)
75+
assert access_token_info.token == MOCK_ACCESS_TOKEN
76+
assert access_token_info.expires_on == 1630920323
77+
assert access_token_info.token_type == 'Bearer'
78+
79+
assert msal_cred.acquire_token_scopes == ['https://management.core.windows.net//.default']
80+
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+
85+
sdk_cred.get_token_info('https://management.core.windows.net//.default', options={'claims': MOCK_CLAIMS})
86+
assert msal_cred.acquire_token_claims_challenge == MOCK_CLAIMS
87+
88+
89+
if __name__ == '__main__':
90+
unittest.main()

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,15 @@ def aad_error_handler(error, **kwargs):
5353
raise AuthenticationError(error_description, msal_error=error, recommendation=recommendation)
5454

5555

56-
def _generate_login_command(scopes=None, claims=None):
56+
def _generate_login_command(scopes=None, claims_challenge=None):
5757
login_command = ['az login']
5858

5959
# Rejected by Conditional Access policy, like MFA
6060
if scopes:
6161
login_command.append('--scope {}'.format(' '.join(scopes)))
6262

6363
# Rejected by CAE
64-
if claims:
64+
if claims_challenge:
6565
# Explicit logout is needed: https://github.com/AzureAD/microsoft-authentication-library-for-python/issues/335
6666
return 'az logout\n' + ' '.join(login_command)
6767

0 commit comments

Comments
 (0)