πŸ” Secure Python Azure Function Using Azure Key Vault and Managed Identity

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:

  1. Get your user object ID:
az ad signed-in-user show --query id --output tsv
  1. 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

  1. Get the Managed Identity Principal ID:
az functionapp show \
  --name <your-functionapp-name> \
  --resource-group <your-resource-group> \
  --query identity.principalId --output tsv
  1. Assign the Key Vault Secrets User role:
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

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.