1

I wish to upgrade a console application that uses Azure.ResourceManager to report information about subscriptions and resources in an arbitrary Azure subscription. The console application uses the InteractiveBrowserCredentials to pop up a browser window where users can log in (or, most usually, select the applicable Microsoft account they have previously signed into). The application now requires a more interactive front-end, and porting it to Blazor WASM appeared simple; however, at runtime, the following minimal code fails in Blazor WASM:

var armClient = new ArmClient(new InteractiveBrowserCredential());
var subs = armClient.GetSubscriptions().ToArray();

It throws an exception because Monitors can not await on the WASM runtime. InteractiveBrowserCredential authentication failed: Cannot wait on monitors on this runtime.

It is not feasible to add an App Registration to the Azure AD in advance to make this work due to the generic utility nature of the tool; the user should interactively authenticate during the running of the utility.

The console application (and applications like Visual Studio Code) prompts for the necessary credentials at runtime.

What is the best way to replicate the InteractiveBrowserCredential flow from a standalone WASM application?

Ray Hayes
  • 14,896
  • 8
  • 53
  • 78

1 Answers1

0

I added a custom TokenCredential implementation called BearerTokenCredential

public class BearerTokenCredential : TokenCredential
{
    private readonly IAccessTokenProvider _tokenProvider;
    private readonly NavigationManager _navigation;
    private readonly IEnumerable<string> _scopes;

    public BearerTokenCredential(IAccessTokenProvider tokenProvider, NavigationManager navigation, IEnumerable<string> scopes)
    {
        _tokenProvider = tokenProvider;
        _navigation = navigation;
        _scopes = scopes;
    }

    public override Azure.Core.AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
    {
        var tokenResult = _tokenProvider.RequestAccessToken()
            .AsTask()
            .GetAwaiter()
            .GetResult();

        if (tokenResult.TryGetToken(out var token)) {
            return new Azure.Core.AccessToken(token.Value, token.Expires);
        }
        else {
            throw new AccessTokenNotAvailableException(_navigation, tokenResult, _scopes);
        }
    }

    public override async ValueTask<Azure.Core.AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
    {
        var tokenResult = await _tokenProvider.RequestAccessToken()
            .ConfigureAwait(false);

        if (tokenResult.TryGetToken(out var token)) {
            return new Azure.Core.AccessToken(token.Value, token.Expires);
        }
        else {
            throw new AccessTokenNotAvailableException(_navigation, tokenResult, _scopes);
        }
    }
}

This service uses the IAccessTokenProvider class to retrieve access tokens as described in the Blazor documentation for Security and Identity (Additional scenarios). The AccessTokenNotAvailableException will redirect the user to be reauthenticated when thrown.

Then I register the ArmClient service in my IServiceCollection via the Dependency Injection extension methods as a singleton (as is recommended). I need to build the service provider here to access the IAccessTokenProvider interface and NavigationManager class.

var scopes = new List<string>() {
    "https://management.azure.com/.default"
};

builder.Services.AddAzureClients(b => {
    var sp = builder.Services.BuildServiceProvider();

    var tokenProvider = sp.GetRequiredService<IAccessTokenProvider>();
    var navigation = sp.GetRequiredService<NavigationManager>();

    b.AddClient<ArmClient, ArmClientOptions>(o => {
        return new ArmClient(new BearerTokenCredential(tokenProvider, navigation, scopes));
    });
});

Then finally I can inject the ArmClient in my pages and use it.

@inject ArmClient client;

@code {
    private List<SubscriptionResource> subscriptions = new();

    protected override async Task OnInitializedAsync()
    {
        subscriptions = await client.GetSubscriptions()
            .GetAllAsync()
            .ToListAsync();
    }
}

However, I am unsure about the BearerTokenCredential implementation, especially the synchronous GetToken method and the building of the IServiceProvider in Program.cs, maybe someone else can weigh in on that.

Tiamo Idzenga
  • 1,006
  • 11
  • 23