17

I am trying to programmatically retrieve the HostedServices from Microsoft.Azure.Management.Compute using C#. This requires ServiceClientCredential and I do not know how to get it.

How can I instantiate this class?

I am able to get them using Microsoft.WindowsAzure.Management.Compute but here it returns only the instances under ResourceManager not the classic instances.

KyleMit
  • 45,382
  • 53
  • 367
  • 544
user5748653
  • 171
  • 1
  • 1
  • 5
  • 8
    Indeed, for some reason Microsoft has decided to provide only the most minimal documentation about these libraries. It is very confusing, especially since Classic and Resource Manager are apparently(?) handled in different libraries, with confusing namespaces like Microsoft.Azure.Management.Compute (does one thing) vs Microsoft.WindowsAzure.Management.Compute (does something different) – Nik Mar 17 '16 at 19:33
  • 1
    I have been trying to figure out this for last few days now and the more I read their documentation, the more I get confused ! – kuldeep Mar 10 '17 at 09:30

4 Answers4

21

First you need to create Active Directory application. See How to: Use the portal to create an Azure AD application and service principal that can access resources

The sample code below uses the nuget package Microsoft.Azure.Management.Compute 13.0.1-prerelease:

public class CustomLoginCredentials : ServiceClientCredentials
{
    private string AuthenticationToken { get; set; }
    public override void InitializeServiceClient<T>(ServiceClient<T> client)
    {
        var authenticationContext = new AuthenticationContext("https://login.windows.net/{tenantID}");
        var credential = new ClientCredential(clientId: "xxxxx-xxxx-xx-xxxx-xxx", clientSecret: "{clientSecret}");

        var result = authenticationContext.AcquireToken(resource: "https://management.core.windows.net/", clientCredential: credential);

        if (result == null) throw new InvalidOperationException("Failed to obtain the JWT token");

        AuthenticationToken = result.AccessToken;
    }
    public override async Task ProcessHttpRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (request == null) throw new ArgumentNullException("request");

        if (AuthenticationToken == null) throw new InvalidOperationException("Token Provider Cannot Be Null");
        
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", AuthenticationToken);
        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        //request.Version = new Version(apiVersion);
        await base.ProcessHttpRequestAsync(request, cancellationToken);
    }
}

Then you can initialize the client like this:

netClient = new Microsoft.Azure.Management.Compute.ComputeManagementClient(new CustomLoginCredentials());
netClient.SubscriptionId = _subscriptionId;
KyleMit
  • 45,382
  • 53
  • 367
  • 544
Ahmed Ashmawy
  • 211
  • 2
  • 4
  • 6
    why do we need to do so much in InitializeServiceClient? It seems like it'd be better to make a constructor that can take the necessary resources and produce the token provider, given that we don't use client to do this. `ProcessHttpRequestAsync` can then Acquire a new token from the auth context (to ensure it's an up to date current token) – ElFik Nov 02 '17 at 23:02
  • 1
    indeed, @ElFik. The synchronous call to `AcquireToken` is no longer available, thus if we don't want to plug in your own async blocking hacks, now we have to move the token acquisition to the `RequestAsync` call as you suggested. – shannon Sep 09 '19 at 01:53
4

The way you'd do this now is to use ITokenProvider and Microsoft.Rest.TokenCredentials.

public class CustomTokenProvider : ITokenProvider
{
    private readonly CustomConfiguration _config;

    public CustomTokenProvider(CustomConfiguration config)
    {
        _config = config;
    }

    public async Task<AuthenticationHeaderValue> GetAuthenticationHeaderAsync(CancellationToken cancellationToken)
    {
        // For app only authentication, we need the specific tenant id in the authority url
        var tenantSpecificUrl = $"https://login.microsoftonline.com/{_config.TenantId}/";

        // Create a confidential client to authorize the app with the AAD app
        IConfidentialClientApplication clientApp = ConfidentialClientApplicationBuilder
                                                                        .Create(_config.ClientId)
                                                                        .WithClientSecret(_config.ClientSecret)
                                                                        .WithAuthority(tenantSpecificUrl)
                                                                        .Build();
        // Make a client call if Access token is not available in cache
        var authenticationResult = await clientApp
            .AcquireTokenForClient(new List<string> { _config.Scope })
            .ExecuteAsync();


        return new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken);
    }
}

And then in your DI configuration

services.AddTransient<IPowerBIClient, PowerBIClient>((provider) =>
{
    var config = provider.GetRequiredService<CustomConfiguration>();
    var tokenProvider = provider.GetRequiredService<CustomTokenProvider>();

    return new PowerBIClient(new Uri(config.BaseUrl), new TokenCredentials(tokenProvider));
});

My example is used with Power BI but would work with anything that needs access to ServiceClientCredentials.

You can use the Nuget package Microsoft.Identity.Client for IConfidentialClientApplication.

antdev
  • 41
  • 2
2

A bit later in the game, but this is how we do this in our project. We use the token credentials that is provided by the .net framework to access a managed identity, or visual studio (code) identity, or interactive. And connect to the azure infrastructure API.

internal class CustomTokenProvider : ServiceClientCredentials
{
    private const string BearerTokenType = "Bearer";
    private TokenCredential _tokenCredential;
    private readonly string[] _scopes;
    private readonly IMemoryCache _cache;

    public CustomTokenProvider(TokenCredential tokenCredential, string[] scopes, IMemoryCache cache)
    {
        _tokenCredential = tokenCredential ?? throw new ArgumentNullException(nameof(tokenCredential));
        _scopes = scopes ?? throw new ArgumentNullException(nameof(scopes));
        _cache = cache;
    }

    public override async Task ProcessHttpRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (request == null)
        {
            throw new ArgumentNullException(nameof(request));
        }

        var token = await _cache.GetOrCreateAsync("accessToken-tokenProvider." + string.Join("#", _scopes), async e =>
        {
            var accessToken = await _tokenCredential.GetTokenAsync(new TokenRequestContext(_scopes), cancellationToken);
            e.AbsoluteExpiration = accessToken.ExpiresOn;
            return accessToken.Token;
        });
        request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(BearerTokenType, token);
        await base.ProcessHttpRequestAsync(request, cancellationToken).ConfigureAwait(false);
    }
}

Couple of remarks:

  • The TokenCredential class does not cache tokens and if you don't do it, it will trigger an error at azure due to excessive requests.
  • Calling a v1 endpoint with v2 calls requires to be a bit creative in the scopes. So when you need to access the management API, provide the following scope "https://management.core.windows.net/.default" and not the user_impersonate scope as specified. This due to some internal conversion on the different endpoints. And '.default' scope is always available and will give yout the on
verbedr
  • 647
  • 6
  • 11
0

As @verbedr answered that you can adapt a TokenCredential from the Azure.Identity client library. @antdev answered that you could implement a Microsoft.Rest.ITokenProvider. Another option is to combine both approaches like so:

using Azure.Core;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Rest
{
    /// Allows an Azure.Core.TokenCredential to be the Microsoft.Rest.ITokenProvider.
    public class TokenCredentialTokenProvider : Microsoft.Rest.ITokenProvider
    {
        readonly TokenCredential _tokenCredential;
        readonly string[] _scopes;

        public TokenCredentialTokenProvider(TokenCredential tokenCredential, string[] scopes)
        {
            _tokenCredential = tokenCredential;
            _scopes = scopes;
        }

        public async Task<AuthenticationHeaderValue> GetAuthenticationHeaderAsync(CancellationToken cancellationToken)
        {
            var accessToken = await _tokenCredential.GetTokenAsync(new TokenRequestContext(_scopes), cancellationToken);
            return new AuthenticationHeaderValue("Bearer", accessToken.Token);
        }
    }
}

It does not have the caching. You could create a CachingTokenProvider or similar if you needed it. This can be used like so:

            var tokenCredentials = new Azure.Identity.DefaultAzureCredential(new Azure.Identity.DefaultAzureCredentialOptions
            {
                AuthorityHost = Azure.Identity.AzureAuthorityHosts.AzurePublicCloud
            });

            var restTokenProvider = new Microsoft.Rest.TokenCredentialTokenProvider(tokenCredentials,
                new string[] { "https://management.core.windows.net/.default" }
            );

            var restTokenCredentials = new Microsoft.Rest.TokenCredentials(restTokenProvider);

            using var computeClient = new ComputeManagementClient(restTokenCredentials);
            // computeClient.BaseUri = // set if using another cloud
            computeClient.SubscriptionId = subscriptionId;
            var vms = computeClient.VirtualMachines.ListAll();
            Console.WriteLine("# of vms " + vms.Count());

This worked for me. Here were the relevant dependencies in my csproj that I used:

    <PackageReference Include="Azure.Identity" Version="1.4.0" />
    <PackageReference Include="Microsoft.Rest.ClientRuntime" Version="2.3.23" />
    <PackageReference Include="Microsoft.Azure.Management.Compute" Version="46.0.0" />
Cameron Taggart
  • 5,047
  • 3
  • 35
  • 62
  • It is worth mentioning that the newly designed Azure.ResourceManager libraries like Azure.ResourceManager.Compute have direct support for Azure.Identity. See https://github.com/Azure/azure-sdk-for-net/blob/master/doc/mgmt_preview_quickstart.md – Cameron Taggart May 21 '21 at 03:28