Skip to main content

Command Palette

Search for a command to run...

Authentication Tokens for Azure AI Foundry Data Agents through Entra Service Principal -Part 2

Updated
6 min read
Authentication Tokens for Azure AI Foundry Data Agents through Entra Service Principal -Part 2

This is Part 2 on the topic of Authentication Tokens for Azure AI Foundry Data Agents through Service Principal.

You can read Part 1 here.

In Part 1, I introduced how to use ClientSecretCredential instead of DefaultAzureCredential to generate an authentication token that uses OAuth 2.0 client credentials flow.

I mentioned that storing client secrets might pose a security risk if not handled properly. Its akin to storing user passwords in the config file of an email application. Such a behavior would be looked as a huge security risk but somehow storing client secrets of service principal is norm.

To mitigate such risk, using Managed Identity is a recommended alternative or another alternative would be to override the TokenCredentials and generate a JwtSecurityToken that acts as the access token, the process that I will be detailing in this article.

Also in Part 1, I mentioned that the defined scope for the process in Part 2 will be different from the one used in Part 1.

In Part 1 , we had used https://cognitiveservices.azure.com/.default as the scope but here we will use https://ai.azure.com/.default

The reason being that https://cognitiveservices.azure.com/.default is the scope for Azure Foundry while https://ai.azure.com/.default is the scope in Entra for Azure AI services.

We will be assigning API permissions to the service principal and not IAM role permissions like we did in Part 1.

Note :

In Part 1 there was an issue with insufficient privileges after assigning Microsoft’s recommended Search Index Data Contributor and Search Service Contributor roles to the Azure Foundry resource.

Following is a screenshot excerpt from Part 1 on the topic :

As mentioned earlier, we will set API permissions to the service principal instead of IAM role that we did in Part 1.

Setup

We start with setting up the API permissions of “Azure Machine Learning Services” to our service principal.

Azure AI Foundry

If we don’t assign the required API permission, the authentication will error out with the following message.

Azure AI Foundry

If you look closely at the error above where I highlighted the Resource app ID in red, you’ll see that its value matches the one I marked in the previous screenshot.

Once the “Azure Machine Learning Services” API permission is assigned to the service principal, the assigned permission should be visible under the Configured permissions window under API permissions.

Azure AI Foundry

Now that we have the set up ready, we create a new C# console application.

Code

Add the following references/NuGet packages through the following .NET CLI commands to the console application.

dotnet add package Azure.Core --version 1.49.0
dotnet add package Azure.AI.Agents.Persistent --version 1.1.0
dotnet add package Azure.AI.Projects --version 1.0.0
dotnet add package Azure.Identity --version 1.16.0
dotnet add package System.IdentityModel.Tokens.Jwt --version 8.14.0

In the next step declare the required variables

static string projectEndpoint = "Your AI foundry resource endpoint";
static string modelDeploymentName = "gpt-4.1";
static string[] scopes = new string[] { "https://ai.azure.com/.default" };
static string tenantId = "Service Principal Tenant Id";
static string clientId = "Service Principal Client Id";
static string AccessToken;

Note that above, we have not declared a variable to store clientSecret the way we did in Part 1.

We then declare a class to override the ClientSecretCredentialclass and generate a JwtSecurityTokenfrom TokenRequestContext.

public class AccessTokenCredential : ClientSecretCredential
{

    public AccessTokenCredential(string accessToken)
    {
        AccessToken = accessToken;
    }

    private string AccessToken;

    public AccessToken FetchAccessToken()
    {
        JwtSecurityToken token = new JwtSecurityToken(AccessToken);
        return new AccessToken(AccessToken, token.ValidTo);
    }

    public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
    {
        return new ValueTask<AccessToken>(FetchAccessToken());
    }

    public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
    {
        JwtSecurityToken token = new JwtSecurityToken(AccessToken);
        return new AccessToken(AccessToken, token.ValidTo);
    }

}

Then, we use PublicClientApplicationBuilder class from Microsoft Authentication Library (MSAL) for .NET to generate an AccessToken.

  public static async Task<AuthenticationResult> ClientApplicationBuilder(string[] Scopes)
  {

      PublicClientApplicationBuilder PublicClientAppBuilder =
          PublicClientApplicationBuilder.Create(clientId)
          .WithAuthority("https://login.microsoftonline.com/organizations")
          .WithCacheOptions(CacheOptions.EnableSharedCacheOptions)
          .WithRedirectUri("http://localhost");

      IPublicClientApplication PublicClientApplication = PublicClientAppBuilder.Build();
      var accounts = await PublicClientApplication.GetAccountsAsync();
      AuthenticationResult result;
      try
      {

          result = await PublicClientApplication.AcquireTokenSilent(Scopes, accounts.First())
                           .ExecuteAsync()
                           .ConfigureAwait(false);
      }
      catch
      {
          result = await PublicClientApplication.AcquireTokenInteractive(Scopes)
                           .ExecuteAsync()
                           .ConfigureAwait(false);
      }

      return result;
  }
💡
Note : To understand more about MSAL, I would strongly recommend to read Microsoft’s official documentation on MSAL here. The above methods are explained in depth in my other article here.

Next, we declare a function that calls PublicClientApplicationBuilder defined earlier.

 public static async Task<string> GenerateAccessTokenForAzureAI()
 {
     AuthenticationResult result = await ClientApplicationBuilder(scopes);
     return result.AccessToken;
 }

Now that we have all set, we call these functions in our console application.

We will do that through the Main function aka entry point of our console application.

  public async static Task Main(string[] args)
  {
      Console.WriteLine("Creating access token  at " + DateTime.Now.ToString());
       Console.WriteLine("");

         var response_token = await GenerateAccessTokenForAzureAI();

         AccessTokenCredential accessTokenCredential = new(response_token);

         Console.WriteLine("Created access token  at " + DateTime.Now.ToString());
         Console.WriteLine("");
         Console.WriteLine("Creating ProjectClient and Data Agent  at " + DateTime.Now.ToString());
         Console.WriteLine("");

         AIProjectClient projectClient = new(new Uri(projectEndpoint), accessTokenCredential);
         PersistentAgentsClient agentsClient = projectClient.GetPersistentAgentsClient();

         PersistentAgent agent = agentsClient.Administration.CreateAgent(
           model: modelDeploymentName,
           name: "My First AI foundry agent",
           instructions: "You are the first Azure foundry AI Agent"
         );

         PersistentAgentThread thread = agentsClient.Threads.CreateThread();
         Console.WriteLine(agent.Name + " created at " + agent.CreatedAt.ToLocalTime().ToString());
  }

Run the console application and approve the permission request on the Microsoft Authenticator app and you will see a Data Agent with name “My First AI foundry agent" created in Azure Foundry.

Complete Code

using Azure.AI.Agents.Persistent;
using Azure.AI.Projects;
using Azure.Core;
using Azure.Identity;
using Microsoft.Identity.Client;
using System.IdentityModel.Tokens.Jwt;

namespace AzureAIConsoleApplication
{

    internal class Program
    {
       static string projectEndpoint = "Your AI foundry resource endpoint";
       static string modelDeploymentName = "gpt-4.1";
       static string[] scopes = new string[] { "https://ai.azure.com/.default" };
       static string tenantId = "Service Principal Tenant Id";
       static string clientId = "Service Principal Client Id";
       static string AccessToken;

        public async static Task Main(string[] args)
        {

            Console.WriteLine("Creating access token  at " + DateTime.Now.ToString());
            Console.WriteLine("");

            var response_token = await GenerateAccessTokenForAzureAI();

            AccessTokenCredential accessTokenCredential = new(response_token);

            Console.WriteLine("Created access token  at " + DateTime.Now.ToString());
            Console.WriteLine("");
            Console.WriteLine("Creating ProjectClient and  Data Agent  at " + DateTime.Now.ToString());
            Console.WriteLine("");

            AIProjectClient projectClient = new(new Uri(projectEndpoint), accessTokenCredential);
            PersistentAgentsClient agentsClient = projectClient.GetPersistentAgentsClient();

            PersistentAgent agent = agentsClient.Administration.CreateAgent(
              model: modelDeploymentName,
              name: "My First AI foundry agent",
              instructions: "You are the first Azure foundry AI Agent"
            );

            PersistentAgentThread thread = agentsClient.Threads.CreateThread();
            Console.WriteLine(agent.Name + " created on " + agent.CreatedAt.ToLocalTime().ToString());
        }

        public static async Task<string> GenerateAccessTokenForAzureAI()
        {

            AuthenticationResult result = await ClientApplicationBuilder(scopes);
            return result.AccessToken;
        }

        public static async Task<AuthenticationResult> ClientApplicationBuilder(string[] Scopes)
        {


            PublicClientApplicationBuilder PublicClientAppBuilder =
                PublicClientApplicationBuilder.Create(clientId)
                .WithAuthority("https://login.microsoftonline.com/organizations")
                .WithCacheOptions(CacheOptions.EnableSharedCacheOptions)
                .WithRedirectUri("http://localhost");

            IPublicClientApplication PublicClientApplication = PublicClientAppBuilder.Build();
            var accounts = await PublicClientApplication.GetAccountsAsync();
            AuthenticationResult result;

            try
            {

                result = await PublicClientApplication.AcquireTokenSilent(Scopes, accounts.First())
                                 .ExecuteAsync()
                                 .ConfigureAwait(false);

            }
            catch
            {
                result = await PublicClientApplication.AcquireTokenInteractive(Scopes)
                                 .ExecuteAsync()
                                 .ConfigureAwait(false);
            }

            return result;
        }

    }

    public class AccessTokenCredential : ClientSecretCredential
    {

        public AccessTokenCredential(string accessToken)
        {
            AccessToken = accessToken;
        }

        private string AccessToken;

        public AccessToken FetchAccessToken()
        {
            JwtSecurityToken token = new JwtSecurityToken(AccessToken);
            return new AccessToken(AccessToken, token.ValidTo);
        }

        public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
        {
            return new ValueTask<AccessToken>(FetchAccessToken());
        }

        public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
        {
            JwtSecurityToken token = new JwtSecurityToken(AccessToken);
            return new AccessToken(AccessToken, token.ValidTo);
        }

    }
}

Conclusion

While storing client secrets for service principals may be a common practice it should not be treated lightly. Just like storing user passwords in configuration files is considered a serious security risk the same level of caution must be applied to client secrets.

To mitigate such risk its better to not store client secrets all together and rather use a process that can create the necessary access tokens only after the required user approval. In this article I tried to highlight one of the methods where such risks can be dealt with.

Thanks for reading !!!

More from this blog

My Ramblings On Microsoft Data Stack

83 posts

From Azure Synapse Analytics, Power BI, Azure Data Factory, Spark and Microsoft Fabric I explore all aspects of the Microsoft Data Stack in this blog.