2

I need to silently authenticate in Azure Blob Storage from a .NET application running on a Windows machine that is domain-joined and the domain is synced to Azure AD.

I am using this example of authentication flow as the base and trying to adapt it for Blob Storage. I successfully obtain a token from AcquireTokenByIntegratedWindowsAuth() method of PublicClientApplication, but I cannot figure out how to supply it to BlobContainerClient. The most appropriate constructor seems to be the one accepting TokenCredential, but I cannot find a suitable class among descendants of TokenCredential.

I ended up writing my own implementation of TokenCredential:

internal class IwaCredential : TokenCredential
{
    private readonly IPublicClientApplication _application;
    private readonly string[] _scopes;

    public IwaCredential(IPublicClientApplication app, string[] scopes)
    {
        _application = app;
        _scopes = scopes;
    }

    private async Task<AuthenticationResult> AuthenticateAsync()
    {
        AuthenticationResult? result = null;
        var accounts = await _application.GetAccountsAsync();

        if (accounts.Any())
        {
            try
            {
                result = await _application.AcquireTokenSilent(_scopes, accounts.FirstOrDefault()).ExecuteAsync();
            }
            catch (MsalUiRequiredException)
            {
            }
        }

        if (result == null)
        {
            result = await _application.AcquireTokenByIntegratedWindowsAuth(_scopes).ExecuteAsync();
        }

        return result;
    }

    private async Task<AccessToken> GetAccessTokenAsync()
    {
        var authResult = await AuthenticateAsync();
        return new AccessToken(authResult.AccessToken, authResult.ExpiresOn);
    }

    public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
    {
        return GetAccessTokenAsync().GetAwaiter().GetResult();
    }

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

Then I am able to pass instance of that to the client:

var appOptions = new PublicClientApplicationOptions
{
    ClientId = "...",
    TenantId = "...",
};
var app = PublicClientApplicationBuilder.CreateWithApplicationOptions(appOptions).Build();
var cred = new IwaCredential(app, new string[] { "https://storage.azure.com/user_impersonation" });
var client = new BlobContainerClient(new Uri("https://foobar.blob.core.windows.net/upload"), cred);

using (Stream file = new FileStream(@"C:\Windows\win.ini", FileMode.Open, FileAccess.Read))
{
    var res = await client.UploadBlobAsync("prefix/win.ini", file);
    Console.WriteLine(res);
}

It works, but I still feel like I am missing something as I believe there should be support for that flow within the standard library.

Am I doing it right way? Please suggest improvements.

greatvovan
  • 2,439
  • 23
  • 43
  • What is the significance of `win.ini`? in this case? I haven't seen that file in-person since Windows 98... – Dai Jul 09 '22 at 00:15
  • BTW, if a method is `async` or otherwise returns a `Task` that originated from another `async` method then the method's name [should have the `Async` suffix](https://stackoverflow.com/q/15951774/159145), so your methods should be named `AuthenticateAsync`, `GetAccessTokenAsync`, etc (yes, this seems silly and verbose at first, but it makes it easy to identify IO boundaries in programs - and it's a stylecop rule enforced in every Microsoft repo/project I know of: – Dai Jul 09 '22 at 00:18
  • Based on the migration guide provided [here](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/app-auth-migration), it seems Windows Integrated Auth is not supported directly by `TokenCredential`. – Gaurav Mantri Jul 09 '22 at 02:56
  • @Dai Async is added, thanks. win.ini is just to provide a sample code that would run on any Windows machine in case anyone wants to run it. On my Win 10 it is almost empty. – greatvovan Jul 10 '22 at 02:08
  • I see @GauravMantri, that's unfortunate. If so, is my implementation good enough or you can suggest any improvements? – greatvovan Jul 10 '22 at 02:10
  • 2
    Your code looks ok to me. You should also raise an issue here as well: https://github.com/Azure/azure-sdk-for-net/tree/main/sdk/identity/Azure.Identity. – Gaurav Mantri Jul 10 '22 at 02:30
  • 1
    Raised: https://github.com/Azure/azure-sdk-for-net/issues/29784 – greatvovan Jul 11 '22 at 19:42

1 Answers1

0

Why not this method using new DefaultAzureCredential(includeInteractiveCredentials: true).

BlobContainerClient blobContainerClient = new BlobContainerClient(
  new Uri(@"https://your-blob-uri.blob.core.windows.net/your-container")
, new DefaultAzureCredential(includeInteractiveCredentials: true));

then pass that BlobContainerClient to UploadFile

public static async Task UploadFile
    (BlobContainerClient containerClient, string localFilePath)
{
    string fileName = Path.GetFileName(localFilePath);
    BlobClient blobClient = containerClient.GetBlobClient(fileName);

    await blobClient.UploadAsync(localFilePath, true);
}
mbw
  • 17
  • 3
  • Reading the documentation, I do not see Integrated Windows Authentication in the list of attempted methods. And you probably meant `includeInteractiveCredentials: false`, because the requirement is to authenticate silently? – greatvovan Oct 07 '22 at 21:32
  • I’ve found that despite the documentation not including this it seems to work consistently both on interactive sessions and also those run as a service. Mind giving it a try and report back if it works for you? The interactive credentials just grabs the running users interactive credentials for authentication. I’m using this for both batch processing and services now on AD joined Azure computers and users. – mbw Oct 07 '22 at 23:52