# Secure AI Functions in Microsoft Agent Framework with Microsoft Entra OAuth Role-Based Access Control 

Lets take a scenario where you have an MAF (Microsoft Agent Framework) **AIAgent** that has tool calls to multiple **AIFunctions.**

You definitely wouldn't want an AIAgent to blindly invoke every available tool or function solely based on a user's prompt or simply because a user requested it. Instead there should be some solid guardrails in place to validate user permissions for invoking a given AIFunction. Before invoking a function, the agent should verify that the user has the required role or scope to perform the requested operation.

The conventional approach is to store user permissions in a database and retrieve them whenever a user logs in. While this works it would require maintaining a separate authorization store, keeping it synchronized with user identities on Entra and performing database lookups to determine user permissions.

But with Microsoft Entra ID , all the requisite roles and permissions are embedded directly into the access token as claims. Once the token has been validated the application can authorize tool calls based on these claims without querying an additional database.

This is more robust approach as the user details and permissions are already centralized in Entra and there is no need maintain custom permission tables or synchronization logic in a database.

### UseCase

Lets use a hypothetical scenario. An AIAgent invokes two AIFunctions

*   **ReturnCountryCapital**
    
*   **ReturnCityTemperature**
    

There are two users

**sachin.nandanwar @ azureguru.net** and **sachin\_nandanwar @ azureguru.net**

> Though I am using Entra users in this example, the same approach can be extended to Entra groups. So instead of assigning roles to individual users you can assign them to groups allowing all group members to inherit the same permissions assigned to the Entra group that they are a member of.

**sachin.nandanwar @ azureguru.net** should have permissions to invoke both the AIFunctions (**ReturnCountryCapital and ReturnCityTemperature)** while **sachin\_nandanwar @ azureguru.net** permissions should be limited to invoke only **ReturnCountryCapital**

### **SetUp**

> This article is heavily influenced by my article on Delegated Tokens and Application tokens wrt setting up role based access in Entra. You can refer to that article [here](https://www.azureguru.net/understanding-delegated-tokens-vs-application-tokens-through-claims-based-authorization-in-microsoft-entra-id).

But I would still cover the same steps from the above article in this article as well.

As mentioned earlier, there are two users

*   **(Sachin.Nand)** **sachin.nandanwar @ azureguru.net**
    
*   **(Sachin Nandanwar)** **sachin\_nandanwar @ azureguru.net**
    

sachin.nandanwar @ azureguru.net will have full permissions to invoke both the AIFunctions (**ReturnCountryCapital and ReturnCityTemperature)** while sachin\_nandanwar @ azureguru.net will have partial permissions limited to invoke only **ReturnCountryCapital** AIFunction

> *Ensure that you the Scalar set up is configured in your* *ASP.NET* *core project. For more details refer to my following article*

[**https://www.azureguru.net/customize-scalar-UI-for-net-api**](https://www.azureguru.net/customize-scalar-UI-for-net-api)

Also ensure that you have a thorough understanding of Minimal API's and implementation of **ClaimPrincipal, Authentication** and **Authorization** for Minimal API's. Please refer to my article below to have a better understanding of the topic.

[**https://www.azureguru.net/implementing-jwt-tokens-in-minimal-api-net-core**](https://www.azureguru.net/implementing-jwt-tokens-in-minimal-api-net-core)

You can ignore the custom JWToken aspect from the above article as in that article the focus was on creating custom JWTokens.

The first step is to create a Service Principal. I created one called **App Service Principal.**

![AI Function access in Microsoft Agent Framework](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/7ecfb62e-9c1c-41dd-b202-e1f9c0bf82e1.png align="center")

In the next step , click the **Expose an API** option. The format of the URI is **api://{ApplicationId}**.

> **ApplicationId above is the Service Principal ClientId**

In the next step , define a scope by clicking the **Add a scope** option.

![AI Function access in Microsoft Agent Framework](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/8f602d97-99f5-4620-801c-19bbee5f3daa.png align="center")

I defined a scope with name **temperature\_capital.read** and used it across all other properties for the scope.

> We will use this scope temperature\_capital.read in our code to generate access token for the service principal on behalf of the signed in user.

In the next step, click **App roles** >> **Create app role**

![AI Function access in Microsoft Agent Framework](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/e3d65fb6-3679-4a13-bf97-c93f354b4104.png align="center")

and define two app roles

*   **Temperature and Capital Read Role**
    
*   **Capital Read Role**
    

> The **Temperature and Capital Read** role is assigned to users who require access to invoke both the R**eturnCountryCapital** and **ReturnCityTemperature** functions while the **Capital Read** role is assigned to users who only need access to invoke the **ReturnCountryCapital** function. Users with this role are denied permission to invoke the **ReturnCityTemperature** function.

Now that we have app roles and API's defined, in the next step , under **API permissions** , click **Add a permission** and select and add the API created in the previous step.

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/9e8c26c8-6368-42d4-bb8b-598e43f23089.png align="center")

![AI Function access in Microsoft Agent Framework](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/142af34b-b53e-458d-829f-4a756bea17d9.png align="center")

> *This might sound counterintuitive to see that we have to assign permissions for an API to the service principal that created that API in the first place. But that’s the way it is. Not doing so results in an Unauthorized access error.*

The next step is to enable **Assignment required** property.

Browse to **Entra ID** >> **Enterprise apps** >> **All applications**

Select your application. In our case it is **App Service Principal**

![AI Function access in Microsoft Agent Framework](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/3cdca9a7-93de-419b-9fb2-2d88457f2144.png align="left")

Ensure that **Assignment required?** is set to **Yes** . By default it is **No**.

![AI Function access in Microsoft Agent Framework](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/3a286463-9ec4-483a-a17f-2be992abbea8.png align="center")

Navigate to **Users and groups** in the same page add click **Add user/group**

![AI Function access in Microsoft Agent Framework](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/c1d52bde-eb0a-419d-8e0f-071d0a4e0bd8.png align="center")

Click **None Selected** and search for the user/group to whom you want to grant Assignments.

![AI Function access in Microsoft Agent Framework](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/99217e5d-3497-472b-ad95-8add93740b33.png align="center")

I granted **Temperature and Capital Read Role** to the user **Sachin Nand** i.e. **sachin.nandanwar @ azureguru.net**

![AI Function access in Microsoft Agent Framework](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/a9d3a4be-0929-4d0a-9ebc-e074c24caf49.png align="center")

I then granted **Capital Read Role** to user **sachin.nandanwar @ azureguru.net** i.e. **Sachin Nandanwar**

![AI Function access in Microsoft Agent Framework](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/4c8bf0e7-cb8c-4a72-965e-6e6e1c427995.png align="center")

User assigned to a role is listed under **Users and groups**

![AI Function access in Microsoft Agent Framework](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/7b85fd80-3051-43d4-a2cb-3c6609bfd15e.png align="center")

Now that the set up and the required prerequisite is in place , lets move on to the code.

### **Code**

Add the following references to your ASP.NET core project

```csharp
dotnet add package Azure;
dotnet add package Azure.Core;
dotnet add package Microsoft.IdentityModel.Tokens;
dotnet add package Scalar.AspNetCore;
dotnet add package System.Security.Claims;
dotnet add package Microsoft.Agents.AI;
dotnet add package Azure.AI.OpenAI;
dotnet add package Microsoft.Extensions.AI;
dotnet add package System.Text.Json.Serialization;
```

Lets first set up the Scalar API interface.

**Scalar.cs >>**

```csharp
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;

internal sealed class BearerSecuritySchemeTransformer : IOpenApiDocumentTransformer
{
    private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;

    public BearerSecuritySchemeTransformer(IAuthenticationSchemeProvider authenticationSchemeProvider)
    {
        _authenticationSchemeProvider = authenticationSchemeProvider;
    }

    public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
    {
        var authenticationSchemes = await _authenticationSchemeProvider.GetAllSchemesAsync();

        if (authenticationSchemes.Any(authScheme => authScheme.Name == "Bearer"))
        {
            document.Components ??= new OpenApiComponents();
            document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();
            document.Components.SecuritySchemes["Bearer"] = new OpenApiSecurityScheme
            {
                Type = SecuritySchemeType.Http,
                Scheme = "bearer",
                In = ParameterLocation.Header,
                BearerFormat = "JWT"
            };

            foreach (var operation in document.Paths.Values.SelectMany(path => path.Operations))
            {
                if (operation.Value.Security == null)
                {
                    operation.Value.Security = new List<OpenApiSecurityRequirement>();
                }
                var securityRequirement = new OpenApiSecurityRequirement
                {
                    [new OpenApiSecuritySchemeReference("Bearer", document)] = []
                };

                operation.Value.Security ??= new List<OpenApiSecurityRequirement>();
                operation.Value.Security.Add(securityRequirement);
            }
        }
    }
}
```

**Scalar UI >>**

```csharp
 builder.Services.AddOpenApi(
    options =>
    {
        options.AddDocumentTransformer<BearerSecuritySchemeTransformer>();
    }
);

WebApplication app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapScalarApiReference(
    options =>
    {
        options.Title = "Scalar API";
        options.DarkMode = true;
        options.Favicon = "path";
        options.DefaultHttpClient = new KeyValuePair<ScalarTarget, ScalarClient>(
            ScalarTarget.CSharp,
            ScalarClient.RestSharp
        );
        options.HideModels = false;
        options.Layout = ScalarLayout.Modern;
        options.ShowSidebar = true;
        options.Authentication = new ScalarAuthenticationOptions
        {
            PreferredSecuritySchemes = new List<string> { "Bearer" }
        };
    }
);
```

The above code customizes the Scalar UI to include the Bearer token section.

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/51e76cd2-4e73-496a-8c3b-1a0896265105.png align="center")

For more details refer to my article on the topic [**here**](https://www.azureguru.net/customize-scalar-UI-for-net-api).

Next , define an **Authentication** class that validates the user's sign-in and requested scopes with Microsoft Entra ID and then returns a JWT token.

> *Unlike implementing custom JWT tokens which I demonstrated in my earlier article* [*here*](https://www.azureguru.net/implementing-jwt-tokens-in-minimal-api-net-core)*, with Microsoft Entra ID-issued tokens there is no need for generating, signing, rotating or validating JWTs ourselves. Microsoft Entra ID auto handles all these aspects at its end.*

**Authentication.cs >>**

```csharp
    using Microsoft.Identity.Client;
    using System.IdentityModel.Tokens.Jwt;

    namespace Security
    {
        internal class Authentication
        {
            public static string clientId = "Service Principal Client Id";
            private static string[] scopes = { $"api://{clientId}/.default" };
            private static string Authority = "https://login.microsoftonline.com/organizations";
            private static string RedirectURI = "http://localhost";

            public async static Task<JwtSecurityToken> ReturnAuthenticationResult()
            {
                string AccessToken;
                IPublicClientApplication PublicClientApplication = PublicClientApplicationBuilder
                    .Create(clientId)
                    .WithAuthority(Authority)
                    .WithCacheOptions(CacheOptions.EnableSharedCacheOptions)
                    .WithRedirectUri(RedirectURI)
                    .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);
                }

                JwtSecurityToken token = new JwtSecurityToken(result.AccessToken);
                return token;
            }
        }
    }
```

> For brevity, I have defined ClientId as a variable. Ideally it should be placed in a config file or as an Environment variable

Scope used is : **api://{Service Principal ClientId}/.default** for the claims token.

Next, add the following code in **Program.cs** to read settings from the config file **appsettings.json**

```csharp
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.Build();
```

**Read credentials and register** **Chatclient and return a ChatClientAgent**

```csharp
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

var credential = new AzureKeyCredential(configuration["AppSettings:ApiKey"]);

servicecollection.AddKeyedChatClient(
    "ChatClient",
    (
        sp =>
            new AzureOpenAIClient(new Uri(configuration["AppSettings:EndPoint"]), credential)
                .GetChatClient(configuration["AppSettings:Chat_DeploymentName"])
   .AsIChatClient()
    )
);

builder.Services.AddSingleton<ChatClientAgent>(sp =>
{
    return new ChatClientAgent(sp.GetKeyedService<IChatClient>("ChatClient"));
});
```

***Authentication*** *\>>*

```csharp

builder.Services.AddAuthentication().AddJwtBearer(options =>
       {
           options.Authority = $"https://login.microsoftonline.com/{TenantId}";
           options.Validate();
           options.MapInboundClaims = false;
           options.TokenValidationParameters = new TokenValidationParameters
           {
               ValidIssuer = $"https://sts.windows.net/{TenantId}/",
               ValidAudience = $"api://{ClientId}",
               ValidateIssuer = true,
               ValidateAudience = true
           };
       });
```

Authentication mechanism is used to validate the incoming tokens. It checks for two parameters, **ValidIssuer** and **ValidAudience** and ensures that the values for these parameters in the bearer token matches with

*   **https://sts.windows.net/{TenantId}/** and **api://{ClientId}**
    
*   **options.MapInboundClaims = false** . This setting is very important wrt validating the claims . Check the following screenshot
    
    ![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/c076ae1b-33a2-417b-9e5d-a4559b767622.png align="center")
    

The claims and the scope format above is a standard JWT format. If **options.MapInboundClaims = true** (which the default) , the claim check through will fail . For example something like

```csharp
var roles = claims.FindFirst("roles")?.Value
```

roles will always be null as it checks for the keyword **"roles"**

But setting **options.MapInboundClaims = false** the format changes

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/8c6cc89c-e97a-4e18-9eda-602544a385ed.png align="center")

***Authorization*** *\>>*

```csharp
builder.Services.AddAuthorization(options =>
  {
      options.AddPolicy("CapitalTemperatureReadAccess", policy =>
      {
          policy.RequireAuthenticatedUser();
          policy.RequireClaim("aud", $"api://{ClientId}");
          policy.RequireClaim("iss", $"https://sts.windows.net/{TenantId}/");
          policy.RequireClaim("scp", "temperature_capital.read");             

      });
  });
```

Create an authorization policy but before that ensure that request contains authenticated users and then check if the claims contain "**aud**", "**scp**", "**iss**" and validate its values.

Next, define Request and Response objects for Temperature and Capital searches

**CityTemperatureSearchRequest & CityTemperatureSearchResponse** **\>>**

```csharp
 public class CityTemperatureSearchRequest
 {
     public string City { get; set; }
 }

 public class CityTemperatureSearchResponse
 {
     public string City { get; set; }
     public string Temperature { get; set; }
 }
```

**CountryCapitalSearchRequest & CountryCapitalSearchResponse >>**

```csharp
 public class CountryCapitalSearchRequest
 {
     public string Country { get; set; }
 }

 public class CountryCapitalSearchResponse
 {
     public string Country { get; set; }
     public string Capital { get; set; }
 }
```

Next, create serialization metadata for both request and response types.

```csharp
[JsonSerializable(typeof(CountryCapitalSearchRequest))]
[JsonSerializable(typeof(CountryCapitalSearchResponse))]
internal sealed partial class CountryCapitalSerializerContext : JsonSerializerContext;


[JsonSerializable(typeof(CityTemperatureSearchRequest))]
[JsonSerializable(typeof(CityTemperatureSearchResponse))]
internal sealed partial class CityTemperatureSerializerContext : JsonSerializerContext;
```

Define a **delegate** named **ReturnCityTemperature** that takes a search request **CityTemperatureSearchRequest** as input and returns a search response **CityTemperatureSearchResponse**.

```csharp
   public static Func<CityTemperatureSearchRequest, CityTemperatureSearchResponse> ReturnCityTemperature = (CityTemperatureSearchRequest) =>

    {
        switch (CityTemperatureSearchRequest.City)
        {

            case "Mumbai":

                return new CityTemperatureSearchResponse
                {

                    City = "Mumbai",
                    Temperature = "40"
                };

                break;

            case "Pune":

                return new CityTemperatureSearchResponse
                {

                    City = "Pune",
                    Temperature = "41"
                };

                break;

            case "Delhi":

                return new CityTemperatureSearchResponse
                {

                    City = "Delhi",
                    Temperature = "42"
                };

                break;

            case "Chennai":

                return new CityTemperatureSearchResponse
                {

                    City = "Chennai",
                    Temperature = "43"
                };
                break;

        }

        return new CityTemperatureSearchResponse
        {

            City = CityTemperatureSearchRequest.City,
            Temperature = "Unknown"
        };

    };
```

Similarly, define a **delegate** named **ReturnCountryCapital** that takes a search request **CountryCapitalSearchRequest** as input and returns a search response **CountryCapitalSearchResponse**.

```csharp
    public static Func<CountryCapitalSearchRequest, CountryCapitalSearchResponse> ReturnCountryCapital = (CountryCapitalSearchRequest) =>

       {
           switch (CountryCapitalSearchRequest.Country)
           {

               case "India":

                   return new CountryCapitalSearchResponse
                   {

                       Country = "India",
                       Capital = "New Delhi"
                   };
                   break;

               case "USA":

                   return new CountryCapitalSearchResponse
                   {

                       Country = "USA",
                       Capital = "Washington"
                   };
                   break;


               case "Germany":

                   return new CountryCapitalSearchResponse
                   {

                       Country = "Germany",
                       Capital = "Berlin"
                   };
                   break;

               case "Russia":

                   return new CountryCapitalSearchResponse
                   {

                       Country = "Russia",
                       Capital = "Moscow"
                   };
                   break;
           }

           return new CountryCapitalSearchResponse
           {

               Country = CountryCapitalSearchRequest.Country,
               Capital = "Unknown"
           };

       };
}
```

Register the above two delegates in the DI container as **AIFunction** with the corresponding **SerializerOptions** defined earlier.

```csharp
  builder.Services.AddSingleton<AIFunction>(sp =>
     {
         return AIFunctionFactory.Create(ReturnCountryCapital, new AIFunctionFactoryOptions { Name = "ReturnCountryCapital", Description = "Gets the capital city for a specific country.", SerializerOptions = CountryCapitalSerializerContext.Default.Options });
     });


  builder.Services.AddSingleton<AIFunction>(sp =>
    {
        return AIFunctionFactory.Create(ReturnCityTemperature, new AIFunctionFactoryOptions { Name = "ReturnCityTemperature", Description = "Gets the temperature of a city", SerializerOptions = CityTemperatureSerializerContext.Default.Options });
    });
```

***Login Endpoint >>***

Here we fetch the access token issued by Entra ID which is part of the OIDC flow.

```csharp
app.MapPost("/login", async() =>
{
tokens = await Security.Authentication.ReturnAuthenticationResult();
return Results.Ok(tokens);
}).WithOpenApi();
```

***Chat Endpoint >>***

```csharp
app.MapGet("/chat", async (string request, ClaimsPrincipal claims) =>
 {
     var aud = claims.FindFirst("aud").Value;
     var issuer = claims.FindFirst("iss").Value;
     var roles = claims.FindFirst("roles").Value;
     var scp = claims.FindFirst("scp").Value;

     var chatclientlist = app.Services.GetRequiredKeyedService<IChatClient>("ChatClient");

     var aifunctions = app.Services.GetServices<AIFunction>();

     List<AITool> functions = new(aifunctions);

     if (aud == $"api://{ClientId}" && issuer == $"https://sts.windows.net/{TenantId}/")
     {
         var options = new ChatClientAgentOptions
         {
                 ChatOptions = new ChatOptions
                 {

                     Tools = functions
                 }
             };
         

         if (roles == "TemperatureCapital.Read" && scp == "temperature_capital.read")
         {
             options.ChatOptions.Instructions = "You return capital city of a country and you also provide temperature of a city.You will use only the data provided to you and not use any external data";

         }

         if (roles == "Capital.Read" && scp == "temperature_capital.read")
         {

             options.ChatOptions.Instructions = "You return ONLY the capital city of a country and if user asks for temperature you respond 'You are not are authorized to access temperature data'. You will use only the data provided to you and not use any external data";
         }

         var agent = chatclientlist.AsAIAgent(options);

         AgentResponse response = await agent.RunAsync(request);

         return Results.Ok(response.Text);
     }

     return Results.Unauthorized();

 }).RequireAuthorization("CapitalTemperatureReadAccess");
```

Let's break down the above code step by step.

Fetch **audience**, **iss(issuer)**, **scp(scopes)** and **roles** values from claims.

```csharp
var aud = claims.FindFirst("aud").Value;
var issuer = claims.FindFirst("iss").Value;
var roles = claims.FindFirst("roles").Value;
var scp = claims.FindFirst("scp").Value;
```

Retrieve **IChatClient** keyed service and **AIFunction** from the service collection and set it to the functions list variable of type **List**

```csharp

var chatclientlist = app.Services.GetRequiredKeyedService<IChatClient>("ChatClient");

var aifunctions = app.Services.GetServices<AIFunction>();

List<AITool> functions = new(aifunctions);
```

Validate **issuer** and **audience** and if validated create **ChatClientAgentOptions and** set the Tools to the above functions list.

```csharp
 if (aud == $"api://{ClientId}" && issuer == $"https://sts.windows.net/{TenantId}/")
 {
     var options = new ChatClientAgentOptions
     {
             ChatOptions = new ChatOptions
             {
                 Tools = functions
             }
         };     

     if (roles == "TemperatureCapital.Read" && scp == "temperature_capital.read")
     {
         options.ChatOptions.Instructions = "You return capital city of a country and you also provide temperature of a city.You will use only the data provided to you and not use any external data";

     }

     if (roles == "Capital.Read" && scp == "temperature_capital.read")
     {

         options.ChatOptions.Instructions = "You return ONLY the capital city of a country and if user asks for temperature you respond 'You are not are authorized to access temperature data'. You will use only the data provided to you and not use any external data";
     }

     var agent = chatclientlist.AsAIAgent(options);
     AgentResponse response = await agent.RunAsync(request);
     return Results.Ok(response.Text);
 }

 return Results.Unauthorized();
```

Then validate **roles** and **scopes** values . If roles is **TemperatureCapital.Read** then set **ChatOptions** instructions to return both capital of the country and temperature of the city and if the roles is **Capital.Read** then set **ChatOptions** instructions to return only the capital and deny any other requests with a custom message **'You are not are authorized to access temperature data'**.

Assign **ChatOptions** to agent derived from **IChatClient** and send the prompt to the agent and return the response.

And if the validations fail, return an **Unauthorized** error.

Login as : **sachin.nandanwar @ azureguru.net** and send the following prompt

```plaintext
What is capital of India and whats the temperature in Mumbai ?
```

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/b9de56b7-f4b5-479c-a438-2fd6b0063be0.png align="center")

Login as : **sachin.nandanwar @ azureguru.net** and send the the same prompt

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/b60b7f79-6696-43d1-a8da-bb5c8d43c85b.png align="center")

and the access to the temperature data is denied.

### Execution

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/c62dd7ef-3408-48f3-a161-271956c82517.gif align="center")

### Closing Notes

Instead of building a separate security layer for your AIAgents to handle permissions for AIFunction invocation , leveraging user claims through Entra issued OAuth tokens can be more robust ,clean and maintainable architecture.

With this architecture you get a centralized control over permissions required for user driven agent function invocation which ensures that users can invoke only the functions they are permitted to access.

Thanks for reading !!!
