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.