I'm attempting to build a web app for end users to manage their VMs in Azure (giving them access/visibility to VMs based on their existing permissions). I'd like to take advantage of the identity provider built into the Azure We App resource (aka 'Easy Auth' feature). This involves the Azure web app resource itself proxying authentication and passes user details to the app code via headers.
From the docs there doesn't appear to be a way to hand off that information to the azure.identity module and generate a credential object. How can I use that information to imitate the credential object so I can still use the SDK?
I can write individual queries against the API via requests.get with the information retrieved from the headers, but that defeats the purpose of using the SDK.
Anyone have some insight they are willing to share? Do I have to abandon either the built in identity provider, or the SDK?
Some examples of what I've tested (code and errors are displayed in the context of a streamlit app)
As per the docs, I have set up an app registration with perms and configured the web app resource's Settings > Authentication like so:
I am using Microsoft (Entra ID) as my identity provider, with the app registration.
import streamlit as st
from azure.identity import DefaultAzureCredential
from azure.mgmt.resource import ResourceManagementClient
if "authenticated" not in st.session_state or not st.session_state.authenticated:
# first auth with cached credential (deployed web app)
st.session_state.authenticated = False
st.session_state.credential = DefaultAzureCredential()
# if credential.supported():
# st.text('supported')
test_client = ResourceManagementClient(st.session_state.credential, subscription_id="72e6300d-ba3d-4094-aede-60df2d005b2e")
test_group_name = test_client.resource_groups.get(resource_group_name='testgroup')
print(test_group_name)
st.session_state.authenticated = True
Authentication occurs (through Easy Auth--through the managed identity provider configured in the web app resources, which happens before the web app's code is interacted with in any way). The user is then directed to the web app, with the following error as a result:
The Token Store setting of the web app's authentication pane makes the tokens available via headers and refreshable (see Token Store) but these headers are not the same thing as the Shared Token Cache that azure.identity's SharedTokenCacheCredential interacts with (azure.identity doesn't have any means of interacting with such headers out of the box).
My thought was if the azure.identity module is able to be used, it would be through populating the Shared Token Cache from the header information and allowing the SharedTokenCacheCredential object to take things from there perhaps? But that involves creating a secured in-memory object.
Another angle I looked into was AuthorizationCodeCredential, which is allows 'redeeming an authorization code previously obtained', but this occurs at a different step of the Oauth process--the tokens generated and passed as headers are the result of the Oauth process already having moved past that step.
There is a rather helpful diagram on the On-Behalf-Of flow hosted on Microsoft Learn:
Notably, when using Easy Auth (or the MSAL package, for that matter) to authenticate the user, it is acting as the 'Application' in the above diagram, even though the only thing it is doing is authentication. The Python SDK is acting as Web API A in the diagram, which may feel counterintuitive when that is where the bulk of your app code lives. Azure's REST API is Web API B in the diagram.
Because the process is not simple, the following will be a summary and not go into detail. If there are question about any of the steps, let me know. This would make a great blog post. . .
Azure Web App with runtime stack of Python 3.8 or later
Key Vault (to hold secrets for your web app)
Python application code which incorporates the Azure SDK for Python (I was also using streamlit, but it should work with other frameworks too).
SDK App registration for the application (for use by body of code that is using the Azure SDK for Python). This is a SEPARATE app registration from the one used for authentication.
Auth App registration for Authentication code (for use by either MSAL or Easy Auth (Settings > Authentication > Add identity provider in the Web App resource)
Variables (in a web app usually passed through Environment Variables, sometimes automatically available, but just representing them here as vars)
import streamlit as st
import os
from azure.identity import OnBehalfOfCredential
from msal_streamlit_authentication import msal_authentication
from streamlit.web.server.websocket_headers import _get_websocket_headers
from azure.mgmt.resource import ResourceManagementClient
#! MSAL ONLY: remove if using Easy Auth
# check if authenticated
if "authenticated" not in st.session_state or not st.session_state.authenticated:
st.session_state.authenticated = False
with st.sidebar:
login_token = msal_authentication(
auth={
"clientId": auth_app_reg_oid,
"authority": f"https://login.microsoftonline.com/{tenant_id}",
"redirectUri": "/.auth/login/entraid/callback",
"postLogoutRedirectUri": "/"
}, # Corresponds to the 'auth' configuration for an MSAL Instance
cache={
"cacheLocation": "sessionStorage",
"storeAuthStateInCookie": False
}, # Corresponds to the 'cache' configuration for an MSAL Instance
login_request={
"scopes": [f"{sdk_app_reg_oid}/user_impersonation"]
}, # Optional
login_button_text="Login", # Optional, defaults to "Login"
logout_button_text="Logout", # Optional, defaults to "Logout"
key=1 # Optional if only a single instance is needed
)
access_token = login_token["accessToken"]
#! Easy Auth ONLY: remove if using MSAL
#? code for Easy Auth, the following gets the token
# headers = _get_websocket_headers()
# # next check if easy auth is enabled and was used
# access_token = headers.get("X-Ms-Token-Aad-Access-Token")
#! From here the code is the same for both Easy Auth and MSAL
# if not authenticated, halt processing until authentication occurs
if not access_token:
st.write("Authenticate to access protected content")
st.stop()
# if authenticated, start the OBO flow
else:
if access_token is not None:
st.session_state.credential = OnBehalfOfCredential(
tenant_id=tenant_id,
client_id=sdk_app_reg_id,
client_secret=sdk_app_reg_secret,
user_assertion=access_token,
)
with st.sidebar:
st.write('authenticated with OBO flow')
else:
with st.sidebar:
st.write('missing access token')
st.session_state.authenticated = True
# test the credential (doesn't show errors until used with a client)
test_client = ResourceManagementClient(
st.session_state.credential,
subscription_id=test_subscription_id
)
test_group_name = test_client.resource_groups.get(resource_group_name=test_rg_name)
print(test_group_name)
Some other resources I found helpful in getting to a working deployment:
the GitHub-hosted Azure Samples python identity example.
MS Learn: Oauth tokens
MS Learn: Delegated Access Primer
MS Learn: On Behalf Of Flow