# Understanding Delegated Tokens vs Application Tokens through Claims-Based Authorization in Microsoft Entra ID 

My earlier [article](https://www.azureguru.net/securing-azure-storage-in-asp-net-core-minimal-api-s-with-microsoft-entra-id-and-openid-connect-oidc) on securing Azure Storage in ASP.NET core Minimal API's extensively leveraged RBAC access control and through **Microsoft Entra ID** for user sign-in through **OpenID Connect (OIDC)** to authenticate and authorized users.

But sometimes there can be scenarios where maintaining RBAC access cannot be feasible . Imagine managing an Azure tenant in an organization that has 1,000+ employees and hundreds of storage accounts and Azure services and where every user requires different access to these services. Though manageable through RBAC but managing and maintaining such a large number of roles can be challenging and cause operational overhead.

An alternative solution can be a claim based authorization through Microsoft Entra ID issued access tokens along with authorization policies.

Instead of assigning Azure RBAC roles to every user for every resource, Microsoft Entra ID can issue access tokens containing claims that describe the user's permissions. The application can then evaluate these claims through authorization policies and determine if the user is allowed to perform a particular operation.

In this article, we will build an **ASP.NET Core Minimal API** that authenticates users with **Microsoft Entra ID.** Once the claims are validated from the access token, the API will securely accesses **Azure ADLS Gen2** Storage through **DataLakeServiceClient** (i.e. return a list of all the directories in a given Azure ADLS Gen2 storage container) on behalf of the user, based on access granted to him through **App roles** in Microsoft Entra.

A sample claim based access token might contain application-specific claims similar to the following:

```plaintext
{
  "sub": "1234567890",
  "name": "ABC",
   "roles": [
        "Storage.Access"
      ],
  "permissions": [
    "storage.read",
    "storage.write"
  ]
}
```

In the above example, the permissions claim is interpreted by the application to determine whether the user is authorized to perform read or write operations on the storage based on the roles .

> Instead of granting RBAC role access to individual user, only the service principal is granted RBAC role on the Azure service, in our case the access is granted on Azure ADLS Gen2 storage to the service principal.

The application authenticates on the Azure Storage using a service principal (or managed identity) that already has the necessary Azure RBAC permissions.

> In such a scenario we require two access tokens. The first token will contain the user claims and the second token will have the necessary scope permissions to perform the underlying actions on the Azure service (in this case Azure ADLS Gen2 storage) on behalf of the user.

The application first validates the claims in the user access token and once its validated it uses the service principal token to authenticate the Azure storage based on the audience value of the delegated scopes assigned to it in Microsoft Entra ID.

The initial steps to be performed are similar to the article below i.e. exposing a service principal API ,defining custom scopes and finally granting permissions to the exposed API.

[https://www.azureguru.net/azure-services-authentication-through-microsoft-managed-identity](https://www.azureguru.net/azure-services-authentication-through-microsoft-managed-identity)

### Flow

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/f4181c61-286c-4d5f-84f3-6ba74f242339.png align="center")

To add more context to the above flow, the scopes used will be

```html
api://{ApplicationId} and https://storage.azure.com
```

created through the **Expose API** option and **Delegated Permissions** for the service principal.

### SetUp

There are two users :

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

Only **(Sachin.Nand)** sachin.nandanwar@azureguru.net will be assigned the necessary **App Role** accesses for the service principal.

> ***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.

Create a Service Principal and grant the **Azure Storage** delegated permissions. I created one called **ADLS GEN2 Service Principal.**

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/769d615c-a697-4527-8aa9-85681f3d8e7c.png align="center")

Grant **Storage Blob Data Owner** role access to the service principal for the storage service.

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/074a71dc-0b44-44a0-aacb-f2a816c1d266.png align="center")

In the next step ,under the **Expose an API** option, click the Add button. By default the Application ID URI is the ClientID of the Service Principal.

The format is api://**{ApplicationId}**.

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/eb968e99-761b-4552-9b39-4b3d61c027f3.png align="center")

Cross check the Application ID URI with the Client ID of the service principal.

In the next step define a new scope.

Click **“Add a scope”** option and enter **storage\_read** as the scope name.

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/fcee0ab0-268d-4572-8527-3bac7e717cb3.png align="center")

Once added, the scope should be visible on the **Expose an API** page.

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/548cc029-3d12-4409-9e1c-4d7222c2917c.png align="center")

In the next step , under **API permissions** , click **Add a permission** and select the API that was created in the previous step.

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/2b39db6f-bd50-45af-996c-60af6b378061.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.

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/47cb090f-3ae3-4fc4-9d23-1d490dbb2aa8.png align="center")

> Please ensure that Azure Storage Delegated permission is also assigned to service principal.

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/024db801-9161-4a8c-af24-cd9f0ff48f65.png align="center")

In the next step, create an App Role.

> App roles (Application Roles) is a feature of Role-Based Access Control (RBAC) that defines what actions a user, group or service is allowed to perform within a specific application at the apps identity level.

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/8bd5f304-7bf0-4198-99ed-13d20b6e6cd3.png align="center")

App role name is **AzureStorage** and value is **AzureStorage.Read**

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/eefa4723-6d5a-4e7e-ba54-e380c3f06bd3.png align="center")

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/c0b741b4-5855-4273-b998-3f226e1ee4b9.png align="center")

The role **AzureStorage** created will be part of the access token. This is very crucial aspect to validate user permissions.

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 **ADLS Gen2 Service Principal**

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/e22925f4-0ec9-453a-a7b5-84dd2e667585.png align="center")

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

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/38aaa21a-abb7-4762-8d35-41c361b1cf32.png align="center")

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

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/4d7bf74e-8e1d-4640-8353-148da7fed0bd.png align="center")

Click **None Selected** and Search for user to whom you want to grant the Assignements.

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/65f94c4b-72fa-4e64-9998-eb554a247c6a.png align="center")

Grant assignment to the selected user . In this case I will assign it only to the user **Sachin Nand**

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/863fd1e3-8e1a-440d-bf6c-496932bf7c64.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.Core;
dotnet add package Azure.Storage.Files.DataLake;
dotnet add package Microsoft.IdentityModel.Tokens;
dotnet add package Scalar.AspNetCore;
dotnet add package System.Security.Claims;
```

We first 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 TenantId = "Tenant Id";
    public static string ClientId = "Client Id of the Service Principal";
    public static string ClientSecret = "Client secret of the Service Principal"   

    private static IEnumerable < string > scopes = new List < string > {
      "https://storage.azure.com/.default",
      "api://{ApplicationID}/.default"
    };

    private static string Authority = $"https://login.microsoftonline.com/{TenantId}";
    private static string RedirectURI = "http://localhost";

    public async static Task < JwtSecurityToken[] > ReturnAuthenticationResult() {
      string AccessToken;
      IPublicClientApplication PublicClientApplication_claims =
        PublicClientApplicationBuilder.Create(clientId)
        .WithAuthority(Authority)
        .WithCacheOptions(CacheOptions.EnableSharedCacheOptions)
        .WithRedirectUri(RedirectURI).Build();

      var accounts_claims = await PublicClientApplication_claims.GetAccountsAsync();
      AuthenticationResult claims_result, storage_result;
      var scopeList = scopes.ToList();
      try {

        claims_result = await PublicClientApplication_claims.AcquireTokenSilent(new [] {
            scopeList.ElementAt(0)
          }, accounts_claims.First())
          .ExecuteAsync()
          .ConfigureAwait(false);

      } catch {
        claims_result = await PublicClientApplication_claims.AcquireTokenInteractive(new [] {
            scopeList.ElementAt(0)
          })
          .ExecuteAsync()
          .ConfigureAwait(false);

      }

      JwtSecurityToken claims_token = new JwtSecurityToken(claims_result.AccessToken);

      var app = ConfidentialClientApplicationBuilder
        .Create(ClientId)
        .WithClientSecret(ClientSecret)
        .WithAuthority(Authority)
        .Build();

      storage_result = await app
        .AcquireTokenForClient(new [] {
          scopeList.ElementAt(1)
        })
        .ExecuteAsync();

      JwtSecurityToken storage_token = new JwtSecurityToken(storage_result.AccessToken);

      return [claims_token, storage_token];

    }
  }
}
```

> ***For brevity I have defined ClientId, ClientSecret and TenantId in variables. Ideally they should be placed in a config file and ClientSecret stored to a secured location like Environment variables or Key vault and their values fetched from there.***

Few important aspects of the above code.

**ReturnAuthenticationResult** function returns two tokens (**storage token** and **claim token**) through array **JwtSecurityToken\[\]** .

```csharp
 return [claims_token, storage_token];
```

The scopes used are : **https://storage.azure.com/.default** for the storage token and **api://{ApplicationID}/.default** for the claims token.

> claims\_token is a delegated user token (idtyp = user) and storage\_token is a service principal token (idtyp = app)

Why two different token types (**dtyp = user** and **idtyp = app**) is required ?

Its because the user no longer has any RBAC role access to the storage but the service principal has (we had granted that earlier).So only service principal token will be authorized for any access to the storage.

**AccessTokenCredential.cs >>**

```csharp
using Azure.Core;
using System.IdentityModel.Tokens.Jwt;

namespace AccesTokenCredentials
{
    public class AccessTokenCredential : Azure.Identity.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);
        }
    }
}
```

> ***The above class converts bearer tokens to TokenCredentials. We will require TokenCredentials for authenticating DataLakeServiceclient and return the directory structure for the ADLS Gen2 storage.***

For more information as to why this is required, please refer to the following article [https://www.azureguru.net/customize-clientsecretcredential-class-for-onelake-authentication-in-microsoft-fabric](https://www.azureguru.net/customize-clientsecretcredential-class-for-onelake-authentication-in-microsoft-fabric)

**Program.cs >>**

```csharp

using AccesTokenCredentials;
using Azure.Core;
using Azure.Storage.Files.DataLake;
using Microsoft.IdentityModel.Tokens;
using Scalar.AspNetCore;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;

public static class Program
{
     public static string TenantId = "Tenant Id";
     public static string ClientId = "Client Id of the Service Principal";
    public static List<string> listoutput = new();
    public static JwtSecurityToken[] tokens;
    private static async Task Main(string[] args)
    {
        WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
        builder.Services.AddAuthentication().AddJwtBearer(options =>
        {
            options.Authority = $"https://login.microsoftonline.com/{TenantId}";
            options.MapInboundClaims = false;
            options.Validate();
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidIssuer = $"https://sts.windows.net/{TenantId}/",
                ValidAudience = $"api://{ClientId}",
                ValidateIssuer = true,
                ValidateAudience = true
            };
        });

        builder.Services.AddAuthorization(options =>
        {
            options.AddPolicy("AzureStorageAccess", policy =>
            {
                policy.RequireAuthenticatedUser();
                policy.RequireClaim("aud", $"api://{ClientId}");
                policy.RequireClaim("iss", $"https://sts.windows.net/{TenantId}/");
                policy.RequireClaim("scp", "storage.read");
                policy.RequireClaim("roles", "AzureStorage.Read");

            });

        });

        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" }
            };
        });

        app.UseHttpsRedirection();


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


        app.MapGet("/accessazurestorage", async (ClaimsPrincipal claims, HttpContext context) =>
        {
            var audience = claims.FindFirst("aud")?.Value;
            var issuer = claims.FindFirst("iss")?.Value;
            var scp = claims.FindFirst("scp")?.Value;
            var roles = claims.FindFirst("roles")?.Value;

            if (audience != $"api://{ClientId}")
            {
                throw new UnauthorizedAccessException("Unauthorized access !!!");
            }

            if (issuer != $"https://sts.windows.net/{TenantId}/")
            {
                throw new UnauthorizedAccessException("Unauthorized access !!!");
            }


            if (scp != "storage.read")
            {
                throw new UnauthorizedAccessException("Unauthorized access !!!");
            }

            var token = context.Request.Headers["Authorization"].ToString().Replace("Bearer ", "");

            DataLakeServiceClient datalake_Service_Client;
            DataLakeFileSystemClient dataLake_FileSystem_Client;
            string dfsUri = $"https://adlsfilestore.dfs.core.windows.net";
            TokenCredential tokenCredential = new AccessTokenCredential(tokens[0].RawData.ToString());
            datalake_Service_Client = new DataLakeServiceClient(new Uri(dfsUri), tokenCredential);
            dataLake_FileSystem_Client = datalake_Service_Client.GetFileSystemClient("customers");
            DataLakeDirectoryClient rootDirectory_ = dataLake_FileSystem_Client.GetDirectoryClient("");
            listoutput.Clear();
            return Results.Ok(new
            {
                Output = TraverseDirectory(rootDirectory_) 

            });
        })
        .RequireAuthorization("AzureStorageAccess");

        app.MapGet("/fakeaccessazurestorage", () =>
        {
        }).RequireAuthorization("AzureStorageAccess");

        app.Run();
    }

    public static async Task<List<string>> TraverseDirectory(DataLakeDirectoryClient directoryClient)
    {

        await foreach (var item in directoryClient.GetPathsAsync())
        {
            if (item.IsDirectory == true)
            {
                listoutput.Add(item.Name);
                string[] split = item.Name.Split("/");
                var subDir = directoryClient.GetSubDirectoryClient(split.Length == 1 ? split[0] : split[split.Length - 1]);
                await TraverseDirectory(subDir);
            }
        }

        return listoutput;
    }
}
```

Lets dissect the major aspects of the above code

We declare an array of type **JwtSecurityToken\[\]** which is used to store the values for **claims\_token** and **storage\_token** returned by the **Authentication** class defined earlier.

```csharp
 public static JwtSecurityToken[] tokens;
```

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

```csharp
builder.Services.AddAuthentication().AddJwtBearer(options =>
{
    options.Authority = $"https://login.microsoftonline.com/{TenantId}";
    options.MapInboundClaims = false;
    options.Validate();
    options.TokenValidationParameters = new TokenValidationParameters
    {
         ValidIssuer = "https://sts.windows.net/{TenantId}/",
         ValidAudience = "https://storage.azure.com",
         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}/**](https://sts.windows.net/%7BTenantId%7D/) and [**https://storage.azure.com**](https://storage.azure.com)
    
*   **options.MapInboundClaims = false** . This setting is very important wrt validating the claims .Check the following screenshot
    

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/617f530e-9ecd-4e72-9b0c-0c1496401bdc.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 the **HttpContext** 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/66869bf9-8fd4-47ee-a8a3-9a6b20d0b2b4.png align="center")

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

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

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

**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).

***Login >>***

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

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

***TraverseDirectory >>***

```csharp
public static async Task<List<string>> TraverseDirectory(DataLakeDirectoryClient directoryClient)
{

    await foreach (var item in directoryClient.GetPathsAsync())
    {
        if (item.IsDirectory == true)
        {
            listoutput.Add(item.Name);
            string[] split = item.Name.Split("/");
            var subDir = directoryClient.GetSubDirectoryClient(split.Length == 1 ? split[0] : split[split.Length - 1]);
            await TraverseDirectory(subDir);
        }
    }

    return listoutput;
}
```

The above code recursively traverses the directory structure of a given Azure container . For more in-depth details on the approach you can refer to my article

[https://www.azureguru.net/retrieve-the-hierarchical-directory-structure-from-azure-adls-gen2-storage](https://www.azureguru.net/retrieve-the-hierarchical-directory-structure-from-azure-adls-gen2-storage)

***Accessazurestorage >>***

```csharp
        app.MapGet("/accessazurestorage", async (ClaimsPrincipal claims, HttpContext context) => {
        var audience = claims.FindFirst("aud")?.Value;
        var issuer = claims.FindFirst("iss")?.Value;
        var scp = claims.FindFirst("scp")?.Value;
        var roles = claims.FindFirst("roles")?.Value;

        if (audience != $ "api://{ClientId}") {
          throw new UnauthorizedAccessException("Unauthorized access !!!");
        }

        if (issuer != $ "https://sts.windows.net/{TenantId}/") {
          throw new UnauthorizedAccessException("Unauthorized access !!!");
        }

        if (scp != "storage.read") {
          throw new UnauthorizedAccessException("Unauthorized access !!!");
        }

        if (roles != "AzureStorage.Read") {
          throw new UnauthorizedAccessException("Unauthorized access !!!");
        }

        DataLakeServiceClient datalake_Service_Client;
        DataLakeFileSystemClient dataLake_FileSystem_Client;
        string dfsUri = $ "https://adlsfilestore.dfs.core.windows.net";

        TokenCredential tokenCredential = new AccessTokenCredential(tokens[0].RawData.ToString());

        datalake_Service_Client = new DataLakeServiceClient(new Uri(dfsUri), tokenCredential);

        dataLake_FileSystem_Client = datalake_Service_Client.GetFileSystemClient("customers");

        DataLakeDirectoryClient rootDirectory_ = dataLake_FileSystem_Client.GetDirectoryClient("");

        listoutput.Clear();

        return Results.Ok(new {
          Output = TraverseDirectory(rootDirectory_)

        });
      })
      .RequireAuthorization("AzureStorageAccess");

    app.Run();
    }
```

The code is pretty straightforward. Validate **audience**, **issuer**, **scp** and **roles** values from claims.

If all the above criteria are met , then fetch the storage\_token from the tokens array of type **JwtSecurityToken\[\]** (declare earlier) and pass the value to **TokenCredential** for **DatalakeServiceClient** which then eventually returns the directory structure for the Azure container **customers**.

Login in with **sachin.nandanwar@azureguru.net**

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/0a7f3036-3840-43e9-9b78-1e3f0e2a898f.png align="center")

Login in with **sachin\_nandanwar@azureguru.net**

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/7b25d234-39dc-4341-93a7-6ef668b8feb1.png align="center")

### Execution >>

![](https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/22b310a6-ea3a-42f5-bcf4-803b545c1c3f.gif align="center")

### Conclusion >>

The two-token pattern keeps user authorization and resource authorization separate. Leveraging Claims-based authorization and Azure RBAC together results in a much cleaner security model. User are authorized to access the application through their claims while the service principal accesses Azure resources using its own identity. This removes the need to grant Azure RBAC permissions to every user across every Azure resource to which user needs access.

I hope this article helps you get started with claims-based authorization and the two-token pattern in Microsoft Entra ID.

Thanks for reading !!!
