0

From an Azure Function I want to enumerate ServicePrincipals and identify those that have credentials that are going to expire soon.

I looked at the Azure SDK, but the documentation is appalling, I don't see any way to do it.

I suspect the Microsoft Graph API might be what I need. But most of the examples I've seen use some sort of interactive authentication to access it, whereas I want the Azure Function to be authenticated itself.

I suspect this might require use of the client credentials flow, but I've found no documentation on how to use that from an Azure Function.

Ideally I would prefer to just be able to use the Function's system assigned Managed Identity to access the Graph API but it seems the only way to grant it the necessary permission is through the use of a PowerShell script, and I don't want to be maintaining any kind of logic in PowerShell scripts.

Does anyone know where there is any documentation that would help with getting this to work?

Neutrino
  • 8,496
  • 4
  • 57
  • 83
  • You’ve said that you don’t want to use PowerShell but haven’t said which language you want to target for this. – Skin May 26 '23 at 21:47
  • 1
    Can you please check this which shows Microsoft Graph sample Azure Function using csharp:https://github.com/microsoftgraph/msgraph-sample-azurefunction-csharp and more inforation at:https://techcommunity.microsoft.com/t5/modern-work-app-consult-blog/using-microsoft-graph-in-an-azure-function/ba-p/317434 – Mehtab Siddique May 31 '23 at 11:19

2 Answers2

1

I used below c# code in my Azure HTTP Trigger Function to get Azure Service principal properties with client credentials flow and it was successful, Refer below:-

My HTTP Trigger Function.cs code:-

I modified the below code from my SO thread answer here to use graph API in Functions.

using Azure.Core;
using Azure.Identity;
using Newtonsoft.Json;
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Azure.Core;
using Azure.Identity;
using Newtonsoft.Json;
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System.Net.Http.Json;

namespace FunctionApp1
{
    public static class Function1
    {
        [FunctionName("Function1")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            var token = await GetAccessToken("<tenant-id>", "<client-id>", "<client-secret>");
            var results = await GetResults(token);

            return new OkObjectResult(results);
        }

        private static async Task<string> GetAccessToken(string tenantId, string clientId, string clientKey)
        {
            var credentials = new ClientSecretCredential(tenantId, clientId, clientKey);
            var result = await credentials.GetTokenAsync(new TokenRequestContext(new[] { "https://graph.microsoft.com/.default"
}), default);
            return result.Token;
        }

        private static async Task<string> GetResults(string token)
        {
            var httpClient = new HttpClient
            {
                BaseAddress = new Uri("https://graph.microsoft.com/v1.0/")
            };

            string URI = $"applications/7297aaca-505f-48c2-9670-3cad6fad99e7";

            httpClient.DefaultRequestHeaders.Remove("Authorization");
            httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);
            HttpResponseMessage response = await httpClient.GetAsync(URI);

            var HttpsResponse = await response.Content.ReadAsStringAsync();
            //var JSONObject = JsonConvert.DeserializeObject<object>(HttpsResponse);

            //return response.StatusCode.ToString();
            return HttpsResponse;
        }
    }
}

Local Output:-

enter image description here

I deployed the Function in Azure Function app and got the desired results, Refer below:-

enter image description here

Reference:-

https://learn.microsoft.com/en-us/graph/api/serviceprincipal-get?view=graph-rest-1.0&tabs=http#code-try-1

SiddheshDesai
  • 3,668
  • 1
  • 2
  • 11
  • That's one way. After some effort I was able to do it using the managed identity though which involves a lot less code and works seamlessly between local and deployed environments. – Neutrino Jun 06 '23 at 16:07
0

I managed to get this to work.

Firstly I created an Azure Function and enabled the system assigned managed identity for it using the guide here

https://learn.microsoft.com/EN-us/azure/azure-functions/functions-identity-based-connections-tutorial

Then I granted access to Graph API to the managed identity using this guide

https://learn.microsoft.com/en-us/azure/app-service/scenario-secure-app-access-microsoft-graph-as-app?tabs=azure-cli

I was then able to access the Graph API from the Azure Function quite simply like this:

[FunctionName("GraphApi2")]
public static async Task<IActionResult> Run(
      [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
      ILogger log)
{
   var result = new StringBuilder();

   try
   {
      string[] scopes = new[] { "https://graph.microsoft.com/.default" };
      var graphServiceClient = new GraphServiceClient(new DefaultAzureCredential(), scopes);

      ApplicationCollectionResponse apps = await graphServiceClient.Applications.GetAsync();
      result.AppendLine("** Applications **");

      foreach (Application app in apps.Value)
      {
         if (app.PasswordCredentials.Count > 0)
         {
            result.AppendLine($"Name: {app.DisplayName}, ID: {app.AppId}");

            foreach (PasswordCredential credential in app.PasswordCredentials)
            {
               result.AppendLine($"   Credential: {credential.DisplayName}, Expiry: {credential.EndDateTime}");
            }
         }
      }
   }
   catch (Exception ex)
   {
      result.AppendLine(ex.ToString());
   }

   return new ObjectResult(result.ToString());
}

The DefaultAzureCredential() call uses the credentials configured in Visual Studio when developing locally, and automatically uses the assigned managed identity when deployed to Azure.

Neutrino
  • 8,496
  • 4
  • 57
  • 83