# Understanding the OAuth Flow for Microsoft Entra OBO(On Behalf Of) Agent Identities -Part 2

My first [article](https://www.azureguru.net/understanding-the-oauth-flow-for-microsoft-entra-obo-on-behalf-of-agent-identities-part-1) on the OAuth Flow for OBO Agent Identities series explored the setup and configuration required for the OBO authentication flow.

In that article, we covered the following setup and configuration steps:

- Configured a Federated Identity Credential (FIC).
- Created a new App Registration to facilitate user token generation.
- Configured an Identifier URI and OAuth 2.0 scope for the Agent Identity Blueprint.
- Configured the `user_impersonation` delegated permission scope for Azure Storage on behalf of the signed-in user.

In this article, we will look into setting up the Azure function and the client application that calls this Azure Function to return the OBO token.

When we finally obtain the Azure Storage access token through OBO, the token claims should have the following :

- **aud**: Azure Storage
- **scp**: should contain `user_impersonation`
- **family_name** and **user_name** : user name details
- **oid**: should identify the signed-in user (user principal id)
- **xms_par_app_azp:** This claim identifies the blueprint that created the agent and is present **only** in agent identity tokens and not in standard app-only tokens.

More details [<mark>here</mark>](https://learn.microsoft.com/en-us/entra/agent-id/how-to-validate-agent-tokens-downstream-api#token-validation-checks)

Before you read ahead, I would suggest you to kindly familiarize with the following setups from my first article.

[Setting up FIC for Agent blue prints](https://www.azureguru.net/understanding-the-oauth-flow-for-microsoft-entra-obo-on-behalf-of-agent-identities-part-1#federated-identity-credentials-fic-and-agent-blue-print)

[Setting up the identifier URI and OAuth2 scope](https://www.azureguru.net/understanding-the-oauth-flow-for-microsoft-entra-obo-on-behalf-of-agent-identities-part-1#setting-up-the-identifier-uri-and-oauth2-scope)

[Configuring app registration for Tc token generation](https://www.azureguru.net/understanding-the-oauth-flow-for-microsoft-entra-obo-on-behalf-of-agent-identities-part-1#entra-application-registration)

[Configuring user_impersonation delegated Scope for Azure Storage](https://www.azureguru.net/understanding-the-oauth-flow-for-microsoft-entra-obo-on-behalf-of-agent-identities-part-1#configuring-userimpersonation-delegated-scope-for-azure-storage)

### **Authentication flow Diagram**

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/d0e08ba6-e752-478f-8a8e-54b3024b9001.png align="center")

To understand the authentication flow, a user token (**Tc**) is first created, followed by a request for the **TUAMI** (User Managed Identity Token)which is then followed by the token **T1** (Exchange Token) and finally the **OBO** token (On Behalf Of Token) is generated for the user.

> **Azure Storage is our final resource for which we acquire the final OBO access token.**

### **Sequence Diagram**

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/7a83869b-5415-4850-9792-d42817346a77.png align="center")

*Image Credit : Microsoft*

### **Key Considerations**

- Resource in this example is Azure storage
- User signs in with his Entra tenant credentials
- The scope that user signs in with in the client application , corresponds to the blue print id and the OAUTH2 scopes of the agent identity which is `api://{Blue Print Id}/access_agent` set [here](https://www.azureguru.net/understanding-the-oauth-flow-for-microsoft-entra-obo-on-behalf-of-agent-identities-part-1#setting-up-the-identifier-uri-and-oauth2-scope)
- The generation of the final token **TR**(Resource Access Token) is through an Azure Function with `aud`: [`https://storage.azure.com`](https://storage.azure.com)
- The Azure function runs on a User Managed Identity and obtains **T1** through Federated token Exchange
- The same managed identity is used in setting up the Federated Credentials for the agent blueprint
- The credentials used are Federated Identity Credential (FIC) of an existing Managed Identity
- The blueprint then presents the credentials to Microsoft Entra.
- It returns TUAMI token through [**<u>ManagedIdentityCredential</u>**](https://learn.microsoft.com/en-us/dotnet/api/azure.identity.managedidentitycredential?view=azure-dotnet)
- The agent blueprint requests an exchange token **T1** through the **TUAMI** token.
- `aud` : `api://AzureADTokenExchange` is used for token exchange
- The Azure Function is secured through the Agent identity blue print identity provider
- Assign `user_impersonation` scopes on Azure storage for the user principal id (can be done through Microsoft Graph Explorer or PowerShell MgGraph)
- ***Token Tc is the user sign in token***
- ***T1 is the token exchange token through FIC generated by the following code snippet***

```javascript
POST /oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

client_id=AgentBlueprint id
&scope="api://AzureADTokenExchange/.default"
&fmi_path=AgentIdentity id
&client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
&client_assertion=TUAMI
&grant_type="client_credentials"
```

`UAMI` >> Federated Managed Identity token

`client_id` >> Agent blueprint id

`fmi_path` >> Agent Identity id i.e. the Application id

**Agent Identity and Blue Print :**

I have an existing Agent Identity named : **Agent Identity** that I am going to use

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/54646ecb-4f6a-419d-9da4-152d25a63da9.png align="center")

and an agent Blue Print named : **Blue Print**

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/06e7f6a9-c3a5-479d-bc4e-924bd1762053.png align="center")

- ***OBO is the final On Behalf Of downstream resource token generated through the following code snippet***

  ```javascript
  POST /oauth2/v2.0/token
  Content-Type: application/x-www-form-urlencoded

  client_id=AgentIdentity id
  &scope="https://storage.azure.com/.default"
  &client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
  &client_assertion={T1}
  &grant_type="urn:ietf:params:oauth:grant-type:jwt-bearer"
  &assertion={Tc(aud=AgentIdentity Blueprint, oid=User)}
  &requested_token_use="on_behalf_of"
  ```

Note : The scope used here is <https://storage.azure.com/.default>

### **Azure Function**

This is an very important aspect in the entire flow.

Create a new Azure Function and add the following references

```csharp
dotnet add package Azure.Core;
dotnet add package Azure.Identity;
dotnet add package Microsoft.AspNetCore.Http;
dotnet add package Microsoft.AspNetCore.Mvc;
dotnet add package System.Text.Json;
```

Add bunch of variables and set up the constructor for logger

```csharp
string TenantId = "Tenant id";
string UserManagedIdentityId = "Client id of user assigned managed identity";
string AgentIdentityId = "Agent identity id";  
string AgentBluePrintId ="Blue Print id of the agent identity" ;

private readonly ILogger<Function1> _logger;
private static readonly HttpClient client = new HttpClient();
public HttpClient Client => client;

public Function1(ILogger<Function1> logger)
{
    _logger = logger;
}
```

I will walk through the process step by step explaining how to obtain different types of token.

Under the function **Function_Return_T1** of type **Get**

```csharp
[Function("Function_Return_T1")]
public async Task<IActionResult> Run(
 [HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req)
{

}
```

add the following code

***TUAMI (Token Managed Identity) >*\>**

We will use [**<u>ManagedIdentityCredential</u>**](https://learn.microsoft.com/en-us/dotnet/api/azure.identity.managedidentitycredential?view=azure-dotnet) to generate the TUAMI token based on the federated credentials through Managed Identity.

```csharp
  var miCredential = new ManagedIdentityCredential(
      ManagedIdentityId.FromUserAssignedClientId(
          UserManagedIdentityId));
  var assertionContext = new TokenRequestContext(
      new[] { "api://AzureADTokenExchange/.default" });

  AccessToken assertionToken;
  try
  {
      assertionToken = await miCredential.GetTokenAsync(
          assertionContext,
          CancellationToken.None);
  }
  catch (Exception ex)
  {
      _logger.LogError(ex, "UAMI assertion token failed");

      return new ObjectResult(new
      {
          stage = "UAMI assertion token acquisition",
          exceptionType = ex.GetType().FullName,
          error = ex.Message,
          details = ex.ToString()
      })
      {
          StatusCode = 500
      };
  }

  string TUAMI = assertionToken.Token;
```

The resulting access token value is stored in the variable **TUAMI .**

***T1 Token* >>**

Earlier in the article I mentioned of using the following request to obtain token **T1**

```javascript
POST /oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

client_id=AgentBlueprint id
&scope="api://AzureADTokenExchange/.default"
&fmi_path=AgentIdentity id
&client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
&client_assertion=TUAMI
&grant_type="client_credentials"
```

Implement the above POST request in C# in the function **Function_Return_T1**

```csharp
var parameters_T1 = new Dictionary<string, string>
 {
      ["client_id"] = AgentBluePrintId,
      ["scope"] = "api://AzureADTokenExchange/.default", 
      ["fmi_path"] = AgentIdentityId,
      ["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt- bearer",
      ["client_assertion"] = TUAMI,
      ["grant_type"] = "client_credentials"
 };

 HttpContent contents = new FormUrlEncodedContent(parameters_T1);

 var HttpMessage_T1 = new HttpRequestMessage
 {
     Method = HttpMethod.Post,
     RequestUri = new Uri($"https://login.microsoftonline.com/{TenantId}/oauth2/v2.0/token"),
     Content = contents
 };

 HttpResponseMessage response_T1 = await client.SendAsync(HttpMessage_T1);

 string T1;

 try
 {
     response_T1.EnsureSuccessStatusCode();

     using JsonDocument doc = JsonDocument.Parse(response_TUAMI.Content.ReadAsStringAsync().Result);

     T1 = doc.RootElement
        .GetProperty("access_token")
        .GetString()!;

 }
 catch (Exception ex)
 {
     _logger.LogError(ex, "Federated token exchange failed");

     return new ObjectResult(new
     {
         stage = "Federated token exchange",
         exceptionType = ex.GetType().FullName,
         error = ex.Message,
         details = ex.ToString()
     })
     {
         StatusCode = 500
     };

 }
```

The resulting access token is stored under variable **T1**.

We will require another function called **Function_Return_OBO** to return the OBO token.

You might ask why ?

Have a look at the following POST request required for generation of the OBO token

```javascript
POST /oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

client_id=AgentIdentity
&scope="https://storage.azure.com/.default"
&client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
&client_assertion={T1}
&assertion={Tc}
&requested_token_use="on_behalf_of"
```

Our earlier function, `Function_Return_T1` generates the **T1** token, which is required as part of the `client_assertion` in the above POST request alongside the user token (**Tc**) used for the `assertion` value above.

> The function `Function_Return_OBO` accepts **Tc**(User token) and **T1** (Exchange token) as its arguments .The client application generates the **Tc** token and will first make a call to the function `Function_Return_T1`. This function returns **T1** value and the client would then invoke the function`Function_Return_OBO`

I hope I am making sense 🙄🙄

Next, implement the above **POST** request to generate the OBO token in C# in a new function called **Function_Return_OBO.**

```csharp
 [Function("Function_Return_OBO")]
 public async Task<IActionResult> Run_1(
 [HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req, string Tc, string T1)
 {

     var parameters_OBO = new Dictionary<string, string>
     {
         ["client_id"] = AgentIdentityId
         ["scope"] = "https://storage.azure.com/.default",
         ["grant_type"] = "urn:ietf:params:oauth:grant-type:jwt-bearer",
         ["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
         ["client_assertion"] = T1,
         ["assertion"] = Tc,
         ["requested_token_use"] = "on_behalf_of"
     };

     HttpContent contents = new FormUrlEncodedContent(parameters_OBO);

     var HttpMessage_OBO = new HttpRequestMessage
     {
         Method = HttpMethod.Post,
         RequestUri = new Uri($"https://login.microsoftonline.com/{TenantId}/oauth2/v2.0/token"),
         Content = contents
     };

     HttpResponseMessage response_OBO = await client.SendAsync(HttpMessage_OBO);
     return new OkObjectResult(new
     {
         reponse = response_OBO.Content.ReadAsStringAsync().Result,
         stage = "Function_Return_OBO",
         message = "Function_Return_OBO success"
     });
 }
```

Deploy the Azure function to Azure. Once deployed you will see two functions

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/3056fbff-0966-4e37-a532-5503389e68d9.png align="center")

### **Authentication for Azure Function**

The functions (**Function_Return_T1** and **Function_Return_OBO**) will be invoked by the client application, so we need to ensure that the user access token(**Tc**) passed by the client application is properly authenticated and validated by the function.

Also recall that earlier in the article I mentioned the following :

> **The scope that user signs in with in the client application , corresponds to the blue print id of the agent identity** `api://{Blue Print Id}/access_agent`

So considering the above two aspects, we will have to set Authentication for the deployed Azure Functions

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/dbf2a3c4-770d-45a9-8a62-ab38ee5fc0d1.png align="center")

For authentication , we configure the value of **Application(client) ID** property of the identity provider to the agent blue print id and the **Allowed token audiences** property value to the scope value of the client application which in this example is `api://d7efbf70-e40b-4413-b763-b542264764bc/access_agent` where `d7efbf70-e40b-4413-b763-b542264764bc` is the blue print id and `access_agent` is the OAUTH2 scope that we configured in the first article. You can check that [here](https://www.azureguru.net/understanding-the-oauth-flow-for-microsoft-entra-obo-on-behalf-of-agent-identities-part-1#setting-up-the-identifier-uri-and-oauth2-scope).

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/6f05ca49-fa1d-494b-aa65-aec918d221da.gif align="center")

The `aud` from the output of **T1** token from the above screencast is the token exchange value generated by Entra during the token exchange.

The **Allowed client applications value** in the set up is set to the client id of the app registration.

I have explained the reason [here](https://www.azureguru.net/understanding-the-oauth-flow-for-microsoft-entra-obo-on-behalf-of-agent-identities-part-1#entra-application-registration) as why we cant use the blueprint id as the client id for Tc token generation.

We can ignore the following warning while setting up authentication.

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/d2379b19-66ff-4879-ab41-78de3203c571.png align="center")

### **Console Client Application**

I used the [**PublicClientApplicationBuilder**](https://learn.microsoft.com/en-us/dotnet/api/microsoft.identity.client.publicclientapplicationbuilder?view=msal-dotnet-latest) class of the Microsoft.Identity namespace from [MSAL](https://learn.microsoft.com/en-us/entra/identity-platform/msal-overview) library for signed in user token generation **Tc**.

**Authentication.cs >>**

```csharp
using Microsoft.Identity.Client;

namespace Security
{
    internal class Authentication
    {
        public static bool istokencached = false;
        public static string clientId = "App registration client Id";
        private static string[] scopes = { "api://{blue print id}/access_agent" };
        private static string Authority = "https://login.microsoftonline.com/organizations";
        private static string RedirectURI = "http://localhost";

        public async static Task<AuthenticationResult> ReturnAuthenticationResult()
        {
            string AccessToken;
            PublicClientApplicationBuilder PublicClientAppBuilder =
                PublicClientApplicationBuildekr.Create(clientId)
                .WithAuthority(Authority)
                .WithCacheOptions(CacheOptions.EnableSharedCacheOptions)
                .WithRedirectUri(RedirectURI);

            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);
            }
            istokencached = true;
            return result;

        }
    }
}
```

**HttpMethod.cs >>**

This class inherits the above class

```csharp
using System.Net.Http.Headers;
using Microsoft.Identity.Client;

namespace Http
{
    internal class HttpMethods : Security.Authentication
    {
        public static string access_token = "";
        public static readonly HttpClient client = new HttpClient();
        protected HttpClient Client => client;

        public async static Task<string> GetAsync(string url)
        {
            AuthenticationResult result = await ReturnAuthenticationResult();
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);

            access_token = result.AccessToken;
            HttpResponseMessage response = await client.GetAsync(url);
            try
            {
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadAsStringAsync();
            }
            catch
            {
                Console.WriteLine(response.Content.ReadAsStringAsync().Result);
                return null;
            }
        }
    }
}
```

**Program.cs >>**

```csharp
using System.Text.Json;

namespace Tokens
{
    internal class Program
    {
        static async Task Main(string[] args)
        {
            await GenerateTokens();
        }

        public async static Task<string> GenerateTokens()
        {
            string baseUrl = $"{Azure Function URL}/api/Function_Return_T1";
            var response_T1 = await Http.HttpMethods.GetAsync(baseUrl);

            using JsonDocument doc_1 = JsonDocument.Parse(response_T1);

            var T1_Token = doc_1.RootElement
                .GetProperty("response")
                .GetString()!;

            baseUrl =
               $"{Azure Function URL}/api/Function_Return_OBO" +
               $"?Tc={Uri.EscapeDataString(Http.HttpMethods.access_token)}" +
               $"&T1={Uri.EscapeDataString(T1_Token)}";

            var response_Tc = await Http.HttpMethods.GetAsync(baseUrl);

            using JsonDocument doc_2 = JsonDocument.Parse(response_Tc);

            var TC_Token = doc_2.RootElement
                .GetProperty("reponse")
                .GetString()!;
            return TC_Token;

        }
    }
}
```

### Tc token

Here we check values of client token **Tc**

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/23328e7a-c6fa-44bc-b630-4dbfb78de48c.gif align="center")

### Execution

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/b2bfde8c-6551-431b-a4e3-6710e43eca2a.gif align="center")

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/6cf05865-3eea-43b8-9239-4885dd492067.png align="center")

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><p>Note the red block above. You will have to Zoom In a bit look at it. The final resource Token is returned as <strong>reponse</strong> and not <strong>response. </strong>So be aware of this misspelling.</p></div>
</div>

This is the reason why in my code I had to parse the response json with keyword **reponse** and not **response**

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/89f72e0d-ad07-46bd-8be7-5f8288cf2150.png align="center")

Now, lets inspect our token :

So our **OBO** token returned the final downstream resource **aud** value which in our example is Azure storage and the **appid** value which is our agent identity id which proves that the agent is acting on behalf of the user and **iss** value which is the issuer for my tenant. Also returned are the user details.

More details [here](https://learn.microsoft.com/en-us/entra/agent-id/how-to-validate-agent-tokens-downstream-api#token-validation-checks). I had to blur my tenant id in the screenshot below.

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/a6ee06a0-2835-452e-b64f-3fced7f8fb78.png align="center")

In this part we have the **oid** which is the user principal id and **scp** with value **user_impersonation** and **xms_par_app_azp** value of blue print id which confirms that this is an agent identity token

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/016e5779-367c-40d7-9fff-feca85f6b365.png align="center")

We did it.. Woo-hoo !!!

### Conclusion

So in this two part series we have delved into all the underlying details of configuring and setting up an OAuth Flow for Microsoft Entra OBO agent identities.

We walked through the process of configuring the necessary permissions using Microsoft Graph APIs and demonstrated how tokens are exchanged across different stages of the flow there by enabling an agent to generate access tokens for the downstream resources (in this example Azure storage) on behalf of an authenticated user.

I hope this series provides a deeper understanding of the OAuth flow for Microsoft Entra Agent Identities and serves as a reference for developers looking to implement secure, delegated access in their agent architectures.

Thanks for reading !!!
