Using Blob storage from ASP.NET Core with Entra ID authentication

This article shows how to implement a secure upload and a secure download in ASP.NET Core using Azure blob storage. The application uses Microsoft Entra ID for authentication and also for access to the Azure Blob storage container.

Code: https://github.com/damienbod/AspNetCoreEntraIdBlobStorage

Blogs in this series

Security architecture

The application is setup to store the file uploads to an Azure Blob storage container. The authentication uses delegated only flows. A user can authenticate into the application using Microsoft Entra ID. The Azure App registration defines App roles to use for access authorization. The roles are used in the enterprise application. Security groups link the users to the roles. The security groups are used in the Azure Blob container where the RBAC is applied using the groups. A SQL database is used to persist the meta data and integrate into the other parts of the application.

Setting up Azure Blob storage

Two roles were created in the Azure App registration. The roles are assigned to groups in the Enterprise application. The users allowed to used to Azure Blob storage are assigned to the groups.

The groups are then used to apply the RBAC roles in the Azure Blob container. The Storage Blob Data Contributor and the Storage Blob Data Reader roles are used.

Authentication

Microsoft Entra ID is used for authentication and implemented using the Microsoft.Identity.Web Nuget packages. The is a standard implementation. Two policies were created to validate the two different roles used in this solution.

string[]? initialScopes = configuration.GetValue<string>
   ("AzureStorage:ScopeForAccessToken")?.Split(' ');

services.AddMicrosoftIdentityWebAppAuthentication(configuration)
    .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
    .AddInMemoryTokenCaches();

services.AddAuthorization(options =>
{
    options.AddPolicy("blob-one-read-policy", policyBlobOneRead =>
    {
        policyBlobOneRead.RequireClaim("roles", ["blobonereadrole", "blobonewriterole"]);
    });
    options.AddPolicy("blob-one-write-policy", policyBlobOneRead =>
    {
        policyBlobOneRead.RequireClaim("roles", ["blobonewriterole"]);
    });
});

services.AddRazorPages().AddMvcOptions(options =>
{
    var policy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
    options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();

Upload

The application uses the IFormFile interface with the file payload and uploads the file to Azure Blob storage. The BlobClient is setup to use Microsoft Entra ID and the meta data is added to the blob.

public BlobDelegatedUploadProvider(DelegatedTokenAcquisitionTokenCredential tokenAcquisitionTokenCredential,
    IConfiguration configuration)
{
    _tokenAcquisitionTokenCredential = tokenAcquisitionTokenCredential;
    _configuration = configuration;
}

[AuthorizeForScopes(Scopes = ["https://storage.azure.com/user_impersonation"])]
public async Task<string> AddNewFile(BlobFileUploadModel blobFileUpload, IFormFile file)
{
    try
    {
        return await PersistFileToAzureStorage(blobFileUpload, file);
    }
    catch (Exception e)
    {
        throw new ApplicationException($"Exception {e}");
    }
}

private async Task<string> PersistFileToAzureStorage(
    BlobFileUploadModel blobFileUpload,
    IFormFile formFile,
    CancellationToken cancellationToken = default)
{
    var storage = _configuration.GetValue<string>("AzureStorage:StorageAndContainerName");
    var fileFullName = $"{storage}/{blobFileUpload.Name}";
    var blobUri = new Uri(fileFullName);

    var blobUploadOptions = new BlobUploadOptions
    {
        Metadata = new Dictionary<string, string?>
        {
            { "uploadedBy", blobFileUpload.UploadedBy },
            { "description", blobFileUpload.Description }
        }
    };

    var blobClient = new BlobClient(blobUri, _tokenAcquisitionTokenCredential);

    var inputStream = formFile.OpenReadStream();
    await blobClient.UploadAsync(inputStream, blobUploadOptions, cancellationToken);

    return $"{blobFileUpload.Name} successfully saved to Azure Blob Storage Container";
}

The DelegatedTokenAcquisitionTokenCredential class is used to get access tokens for the blob upload or download. This uses the existing user delegated session and creates a new access token for the blob storage access.

using Azure.Core;
using Microsoft.Identity.Client;
using Microsoft.Identity.Web;

namespace DelegatedEntraIDBlobStorage.FilesProvider.AzureStorageAccess;

public class DelegatedTokenAcquisitionTokenCredential : TokenCredential
{
    private readonly ITokenAcquisition _tokenAcquisition;
    private readonly IConfiguration _configuration;

    public DelegatedTokenAcquisitionTokenCredential(ITokenAcquisition tokenAcquisition,
        IConfiguration configuration)
    {
        _tokenAcquisition = tokenAcquisition;
        _configuration = configuration;
    }

    public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public override async ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
    {
        string[]? scopes = _configuration["AzureStorage:ScopeForAccessToken"]?.Split(' ');

        if (scopes == null)
        {
            throw new Exception("AzureStorage:ScopeForAccessToken configuration missing");
        }

        AuthenticationResult result = await _tokenAcquisition
            .GetAuthenticationResultForUserAsync(scopes);

        return new AccessToken(result.AccessToken, result.ExpiresOn);
    }
}

Download

The download creates a BlobClient using the user delegated existing session. The file is downloaded directly.

using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Microsoft.Identity.Web;

namespace DelegatedEntraIDBlobStorage.FilesProvider.AzureStorageAccess;

public class BlobDelegatedDownloadProvider
{
    private readonly DelegatedTokenAcquisitionTokenCredential _tokenAcquisitionTokenCredential;
    private readonly IConfiguration _configuration;

    public BlobDelegatedDownloadProvider(DelegatedTokenAcquisitionTokenCredential tokenAcquisitionTokenCredential,
        IConfiguration configuration)
    {
        _tokenAcquisitionTokenCredential = tokenAcquisitionTokenCredential;
        _configuration = configuration;
    }

    [AuthorizeForScopes(Scopes = ["https://storage.azure.com/user_impersonation"])]
    public async Task<Azure.Response<BlobDownloadInfo>> DownloadFile(string fileName)
    {
        var storage = _configuration.GetValue<string>("AzureStorage:StorageAndContainerName");
        var fileFullName = $"{storage}/{fileName}";
        var blobUri = new Uri(fileFullName);
        var blobClient = new BlobClient(blobUri, _tokenAcquisitionTokenCredential);
        return await blobClient.DownloadAsync();
    }
}

Notes

The architecture is simple and has the base features required for a secure solution. Data protection and virus scanning needs to be applied to the files and this can be configured in the Azure Blob storage. The access is controlled to the users in the group. If this needs to be controlled more, the write access can be removed from the users and switched to a service principal. This can have both security advantages and disadvantages. Multiple clients might also need access to files in this solution and the security needs to be enforced. This requires further architecture changes.

Links

https://learn.microsoft.com/en-us/azure/storage/blobs/authorize-access-azure-active-directory

https://docs.microsoft.com/en-us/azure/storage/blobs/storage-blobs-introduction

https://github.com/AzureAD/microsoft-identity-web

4 comments

  1. […] Using Blob storage from ASP.NET Core with Entra ID authentication (Damien Bowden) […]

  2. […] Using Blob storage from ASP.NET Core with Entra ID authentication – Damien Bowden […]

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.