Introduction
This article builds upon the previous tutorial Build a Python Azure Function to Connect with SharePoint Online via Microsoft Graph API where we created a Python Azure Function to access SharePoint Online using Microsoft Graph API. In this enhanced version, we will store client credentials (Client ID, Tenant ID, Client Secret) in Azure Key Vault, access them securely using Managed Identity, and update the Python function accordingly.
Part 1 – Build a Python Azure Function to Connect with SharePoint Online via Microsoft GraphΒ API
β Why Use Azure Key Vault + Managed Identity?
- Keeps secrets out of code and app settings
- Allows rotation of secrets without redeployment
- Avoids hardcoding credentials or exposing them in the Azure Portal
π§± Prerequisites
- Python 3.9
- Azure CLI installed
- Azure Function Core Tools installed
- Existing Azure Function App (Linux)
- Your app registered in Azure AD with Microsoft Graph API permissions
π Step 1: Create an Azure Key Vault and Add Secrets
az keyvault create \
--name <your-keyvault-name> \
--resource-group <your-resource-group> \
--location <your-location> \
--enable-rbac-authorization true
β οΈ Grant Yourself Access to Manage Secrets
If you receive an error like:
Caller is not authorized to perform action on resource.
Then you need to assign yourself the Key Vault Administrator role:
- Get your user object ID:
az ad signed-in-user show --query id --output tsv
- Assign the role:
az role assignment create \
--assignee <your-user-object-id> \
--role "Key Vault Administrator" \
--scope $(az keyvault show --name <your-keyvault-name> --query id --output tsv)
Add secrets
az keyvault secret set --vault-name <your-keyvault-name> --name "client-id" --value <client-id>
az keyvault secret set --vault-name <your-keyvault-name> --name "tenant-id" --value <tenant-id>
az keyvault secret set --vault-name <your-keyvault-name> --name "client-secret" --value <client-secret>
az keyvault secret set --vault-name <your-keyvault-name> --name "scope" --value <scope>
az keyvault secret set --vault-name <your-keyvault-name> --name "sp-hostname" --value <sp-hostname>
az keyvault secret set --vault-name <your-keyvault-name> --name "sp-sitepath" --value <sp-sitepath>
az keyvault secret set --vault-name <your-keyvault-name> --name "sp-listname" --value <sp-listname>
π€ Step 2: Enable System-Assigned Managed Identity for Function App
az functionapp identity assign \
--name <your-functionapp-name> \
--resource-group <your-resource-group>
π Step 3: Assign Key Vault RBAC Role to the Function App
- Get the Managed Identity Principal ID:
az functionapp show \
--name <your-functionapp-name> \
--resource-group <your-resource-group> \
--query identity.principalId --output tsv
- Assign the
Key Vault Secrets Userrole:
az role assignment create \
--assignee <principal-id> \
--role "Key Vault Secrets User" \
--scope $(az keyvault show --name <your-keyvault-name> --query id --output tsv)
βοΈ Step 4: Retrieve Secrets from Azure Key Vault in Python Using Managed Identity
Install the Azure SDK packages:
pip install azure-identity azure-keyvault-secrets
π§ Step 5: Update Python Code to Use Managed Identity and Azure Key Vault
import json
import logging
import os
import requests
from msal import ConfidentialClientApplication
import azure.functions as func
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient
# Set Key Vault URL
KEYVAULT_URL = "https://<key vault name>.vault.azure.net/"
def get_graph_token(tenant_id, client_id, client_secret, scope):
authority = f"https://login.microsoftonline.com/{tenant_id}"
app = ConfidentialClientApplication(
client_id,
authority=authority,
client_credential=client_secret
)
result = app.acquire_token_for_client(scopes=[scope])
if "access_token" in result:
return result['access_token']
else:
raise Exception("Could not acquire token", result.get("error_description"))
def get_sharepoint_site_id(access_token, hostname, site_path):
if site_path:
endpoint = f"https://graph.microsoft.com/v1.0/sites/{hostname}:{'/' + site_path if not site_path.startswith('/') else site_path}"
else:
endpoint = f"https://graph.microsoft.com/v1.0/sites/{hostname}"
headers = {"Authorization": f"Bearer {access_token}"}
response = requests.get(endpoint, headers=headers)
response.raise_for_status()
return response.json().get("id")
def get_list_id(site_id, access_token, list_name):
url = f"https://graph.microsoft.com/v1.0/sites/{site_id}/lists"
headers = {"Authorization": f"Bearer {access_token}"}
response = requests.get(url, headers=headers)
response.raise_for_status()
lists = response.json().get("value", [])
for lst in lists:
if lst.get("name") == list_name:
return lst.get("id")
raise Exception(f"List '{list_name}' not found.")
def get_list_items(site_id, list_id, access_token):
url = f"https://graph.microsoft.com/v1.0/sites/{site_id}/lists/{list_id}/items?$top=10&expand=fields"
headers = {"Authorization": f"Bearer {access_token}"}
response = requests.get(url, headers=headers)
response.raise_for_status()
return response.json().get("value", [])
def main(req: func.HttpRequest) -> func.HttpResponse:
logging.info('Fetching SharePoint list data using Graph API.')
try:
credential = DefaultAzureCredential()
client = SecretClient(vault_url=KEYVAULT_URL, credential=credential)
tenant_id = client.get_secret('tenant-id').value
client_id = client.get_secret('client-id').value
client_secret = client.get_secret('client-secret').value
scope = client.get_secret('scope').value
access_token = get_graph_token(tenant_id, client_id, client_secret, scope)
hostname = client.get_secret('sp-hostname').value
site_path = client.get_secret('sp-sitepath').value
site_id = get_sharepoint_site_id(access_token, hostname, site_path)
list_name = client.get_secret("sp-listname").value
list_id = get_list_id(site_id, access_token, list_name)
items = get_list_items(site_id, list_id, access_token)
return func.HttpResponse(
body=json.dumps(items),
status_code=200,
mimetype="application/json"
)
except Exception as e:
logging.error(str(e))
return func.HttpResponse(
f"Error: {str(e)}",
status_code=500
)
π§ͺ Step 6: Test Locally
Run your function locally:
func start
The DefaultAzureCredential will use the Azure CLI credentials to access the Key Vault during local development.
β Benefits of This Setup
- No secrets in code or deployment artifacts
- Seamless integration with Azure security and monitoring
- Easy to rotate secrets in Key Vault without redeploying
- RBAC provides fine-grained access control
π References
- Azure Key Vault References in App Settings
- Azure Function Managed Identity
- Python Azure Functions
- Azure Key Vault Python SDK
- Assign RBAC roles
Conclussion
We saw how to leverage Azure Key Vault to secure some confidential information and also avoid hard-coding of client secrets and other configuration values. Using this method we can build the secured Azure Function. In future we will see how to extend the code base with other best practices, performance tuning, handling large lists etc.
Happy Codingβ¦
Pingback: Build a Python Azure Function to Connect with SharePoint Online via Microsoft Graph API | Knowledge Share
Pingback: Improving Python Azure Function Performance: Handling Large SharePoint Lists with Throttling, Caching, and Batch Requests | Knowledge Share
Pingback: Robust Authentication with Microsoft Graph API (Using MSAL and Service Principals) | Knowledge Share
Pingback: π Building Resilient Azure Functions: Mastering Microsoft Graph API Calls with Python | Knowledge Share