Skip to content

Commit 62bf49f

Browse files
Ray Luorayluo
Ray Luo
authored andcommitted
PoC: Managed Identity for Azure VM
1 parent bd931a2 commit 62bf49f

File tree

3 files changed

+90
-11
lines changed

3 files changed

+90
-11
lines changed

msal/application.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2001,6 +2001,21 @@ def acquire_token_for_client(self, scopes, claims_challenge=None, **kwargs):
20012001
- an error response would contain "error" and usually "error_description".
20022002
"""
20032003
# TBD: force_refresh behavior
2004+
if self.client_credential is None:
2005+
from .imds import _scope_to_resource, _obtain_token
2006+
response = _obtain_token(
2007+
self.http_client,
2008+
" ".join(map(_scope_to_resource, scopes)),
2009+
client_id=self.client_id, # None for system-assigned, GUID for user-assigned
2010+
)
2011+
if "error" not in response:
2012+
self.token_cache.add(dict(
2013+
client_id=self.client_id,
2014+
scope=response["scope"].split() if "scope" in response else scopes,
2015+
token_endpoint=self.authority.token_endpoint,
2016+
response=response.copy(),
2017+
))
2018+
return response
20042019
if self.authority.tenant.lower() in ["common", "organizations"]:
20052020
warnings.warn(
20062021
"Using /common or /organizations authority "

msal/imds.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# All rights reserved.
3+
#
4+
# This code is licensed under the MIT License.
5+
import json
6+
import logging
7+
try: # Python 2
8+
from urlparse import urlparse
9+
except: # Python 3
10+
from urllib.parse import urlparse
11+
12+
logger = logging.getLogger(__name__)
13+
14+
def _scope_to_resource(scope): # This is an experimental reasonable-effort approach
15+
u = urlparse(scope)
16+
if u.scheme:
17+
return "{}://{}".format(u.scheme, u.netloc)
18+
return scope # There is no much else we can do here
19+
20+
def _obtain_token(http_client, resource, client_id=None):
21+
# Based on https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http
22+
params = {
23+
"api-version": "2018-02-01",
24+
"resource": resource,
25+
}
26+
if client_id:
27+
params["client_id"] = client_id
28+
resp = http_client.get(
29+
"http://169.254.169.254/metadata/identity/oauth2/token",
30+
params=params,
31+
headers={"Metadata": "true"},
32+
)
33+
try:
34+
payload = json.loads(resp.text)
35+
if payload.get("access_token") and payload.get("expires_in"):
36+
return { # Normalizing the payload into OAuth2 format
37+
"access_token": payload["access_token"],
38+
"expires_in": int(payload["expires_in"]),
39+
"resource": payload.get("resource"),
40+
"token_type": payload.get("token_type", "Bearer"),
41+
}
42+
return payload # Typically an error
43+
except ValueError:
44+
logger.debug("IMDS emits unexpected payload: %s", resp.text)
45+
raise
46+

tests/msaltest.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,11 @@ def _input_scopes():
4747
raise ValueError("SSH Cert scope shall be tested by its dedicated functions")
4848
return scopes
4949

50-
def _select_account(app):
50+
def _select_account(app, show_confidential_app_placeholder=False):
5151
accounts = app.get_accounts()
52+
if show_confidential_app_placeholder and isinstance(
53+
app, msal.ConfidentialClientApplication):
54+
accounts.insert(0, {"username": "This Client"})
5255
if accounts:
5356
return _select_options(
5457
accounts,
@@ -60,11 +63,11 @@ def _select_account(app):
6063

6164
def acquire_token_silent(app):
6265
"""acquire_token_silent() - with an account already signed into MSAL Python."""
63-
account = _select_account(app)
66+
account = _select_account(app, show_confidential_app_placeholder=True)
6467
if account:
6568
pprint.pprint(app.acquire_token_silent(
6669
_input_scopes(),
67-
account=account,
70+
account=account if "home_account_id" in account else None,
6871
force_refresh=_input_boolean("Bypass MSAL Python's token cache?"),
6972
))
7073

@@ -138,6 +141,10 @@ def remove_account(app):
138141
app.remove_account(account)
139142
print('Account "{}" and/or its token(s) are signed out from MSAL Python'.format(account["username"]))
140143

144+
def acquire_token_for_client(app):
145+
"""acquire_token_for_client() - Only for confidential client"""
146+
pprint.pprint(app.acquire_token_for_client(_input_scopes()))
147+
141148
def exit(app):
142149
"""Exit"""
143150
bug_link = (
@@ -154,13 +161,12 @@ def main():
154161
{"client_id": AZURE_CLI, "name": "Azure CLI (Correctly configured for MSA-PT)"},
155162
{"client_id": VISUAL_STUDIO, "name": "Visual Studio (Correctly configured for MSA-PT)"},
156163
{"client_id": "95de633a-083e-42f5-b444-a4295d8e9314", "name": "Whiteboard Services (Non MSA-PT app. Accepts AAD & MSA accounts.)"},
164+
{"client_id": None, "client_secret": None, "name": "System-assigned Managed Identity (Only works when running inside a supported environment, such as Azure VM)"},
157165
],
158166
option_renderer=lambda a: a["name"],
159167
header="Impersonate this app (or you can type in the client_id of your own app)",
160168
accept_nonempty_string=True)
161-
app = msal.PublicClientApplication(
162-
chosen_app["client_id"] if isinstance(chosen_app, dict) else chosen_app,
163-
authority=_select_options([
169+
authority = _select_options([
164170
"https://login.microsoftonline.com/common",
165171
"https://login.microsoftonline.com/organizations",
166172
"https://login.microsoftonline.com/microsoft.onmicrosoft.com",
@@ -169,21 +175,33 @@ def main():
169175
],
170176
header="Input authority (Note that MSA-PT apps would NOT use the /common authority)",
171177
accept_nonempty_string=True,
172-
),
173-
allow_broker=_input_boolean("Allow broker? (Azure CLI currently only supports @microsoft.com accounts when enabling broker)"),
174-
)
178+
)
179+
if isinstance(chosen_app, dict) and "client_secret" in chosen_app:
180+
app = msal.ConfidentialClientApplication(
181+
chosen_app["client_id"],
182+
client_credential=chosen_app["client_secret"],
183+
authority=authority,
184+
)
185+
else:
186+
app = msal.PublicClientApplication(
187+
chosen_app["client_id"] if isinstance(chosen_app, dict) else chosen_app,
188+
authority=authority,
189+
allow_broker=_input_boolean("Allow broker? (Azure CLI currently only supports @microsoft.com accounts when enabling broker)"),
190+
)
175191
if _input_boolean("Enable MSAL Python's DEBUG log?"):
176192
logging.basicConfig(level=logging.DEBUG)
177193
while True:
178-
func = _select_options([
194+
func = _select_options(list(filter(None, [
179195
acquire_token_silent,
180196
acquire_token_interactive,
181197
acquire_token_by_username_password,
182198
acquire_ssh_cert_silently,
183199
acquire_ssh_cert_interactive,
184200
remove_account,
201+
acquire_token_for_client if isinstance(
202+
app, msal.ConfidentialClientApplication) else None,
185203
exit,
186-
], option_renderer=lambda f: f.__doc__, header="MSAL Python APIs:")
204+
])), option_renderer=lambda f: f.__doc__, header="MSAL Python APIs:")
187205
try:
188206
func(app)
189207
except ValueError as e:

0 commit comments

Comments
 (0)