AuthProvider with Microsoft Graph Api "Ctor not found" - c#

I'm trying to use Microsoft.Graph Api to get a list of users. I am able to authenticate on the api. I receive the Token and basic profile info upon login.
Then I am trying to use the Microsoft.Graph.Auth SDK/nuget found here to generate my requests. Here is a barebone example of what I am trying to do (you can also find this example in the doc of the package.
public void test()
{
var clientApplication = PublicClientApplicationBuilder
.Create(ClientId)
.WithTenantId(TenantId)
.Build();
var authProvider = new IntegratedWindowsAuthenticationProvider(clientApplication);
var graphClient = new GraphServiceClient(authProvider);
var users = await graphClient.Users
.Request()
.GetAsync();
}
But I get the error System.MissingMethodException: 'Method not found: 'Void Microsoft.Graph.Auth.IntegratedWindowsAuthenticationProvider..ctor(Microsoft.Identity.Client.IPublicClientApplication, System.Collections.Generic.IEnumerable`1<System.String>)'.' before even entering the method test(). The message says it cannot find IntegratedWindowsAuthenticationProvider but the package is installed and I can navigate to the constructor (F12) without issue.
If I remove the line with IntegratedWindowsAuthenticationProvider, the code executes without crashing. And I can authenticate into the Api successfully. I tried moving the line after the successful authentication but I get the same error.

Microsoft.Graph.Auth only supports .Net 4.5 and I use 4.8. I cannot downgrade to 4.5 because of other requirements in the codebase. I tried to modify the open source project but I ran into other issues so I decided to not use this package. I implemented a basic solution containing a few methods to manage the token myself.
Here is the class that handles the cached token
using System.IO;
using System.Security.Cryptography;
using Microsoft.Identity.Client;
namespace Overwatch.AutoCAD.Authentication
{
static class TokenCacheHelper
{
static TokenCacheHelper()
{
CacheFilePath = System.Reflection.Assembly.GetExecutingAssembly().Location + ".msalcache.bin3";
}
/// <summary>
/// Path to the token cache
/// </summary>
public static string CacheFilePath { get; private set; }
private static readonly object FileLock = new object();
public static void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
lock (FileLock)
{
args.TokenCache.DeserializeMsalV3(File.Exists(CacheFilePath)
? ProtectedData.Unprotect(File.ReadAllBytes(CacheFilePath),
null,
DataProtectionScope.CurrentUser)
: null);
}
}
public static void AfterAccessNotification(TokenCacheNotificationArgs args)
{
// if the access operation resulted in a cache update
if (args.HasStateChanged)
{
lock (FileLock)
{
// reflect changesgs in the persistent store
File.WriteAllBytes(CacheFilePath,
ProtectedData.Protect(args.TokenCache.SerializeMsalV3(),
null,
DataProtectionScope.CurrentUser)
);
}
}
}
internal static void EnableSerialization(ITokenCache tokenCache)
{
tokenCache.SetBeforeAccess(BeforeAccessNotification);
tokenCache.SetAfterAccess(AfterAccessNotification);
}
}
}
Here is my modified initial test method
public async void test()
{
AuthenticationResult authResult = await PublicClientApp
.AcquireTokenSilent(scopes, (await PublicClientApp.GetAccountsAsync()).FirstOrDefault())
.ExecuteAsync();
HttpClient client = ApiHelper.CreateHttpClient("https://graph.microsoft.com/v1.0/");
GraphServiceClient graphClient = new GraphServiceClient(new DelegateAuthenticationProvider(async (requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
}));
var users = await graphClient.Users
.Request()
.GetAsync();
}
All you have to do is link the TokenCache helper in the creation of the client app builder
public static void CreateApplication()
{
var builder = PublicClientApplicationBuilder.Create(ClientId)
.WithAuthority($"{Instance}{Tenant}")
.WithRedirectUri("https://login.microsoftonline.com/common/oauth2/nativeclient");
_clientApp = builder.Build();
TokenCacheHelper.EnableSerialization(_clientApp.UserTokenCache);
}
Of course, add try catches and fail safes based on your requirements.

Related

Cache is null on subsequent sign-in to ASP.NET MVC using MSAL

I am trying to cache Access Token using MSAL by following the tutorial provided here: https://github.com/Azure-Samples/ms-identity-aspnet-webapp-openidconnect
I am using ASP.NET MVC on .NET 4.7.2.
But I am getting error when calling an Microsoft Graph API by getting the token from cache.
I'm getting the error when my code hits this line:
result = app.AcquireTokenSilent(scopes, account).ExecuteAsync().Result;
Following the steps when I get the issue.
Run the code from Visual Studio.
Code hit OnAuthorizationCodeReceived()
Able to get the data from Microsoft.Graph
Sign-in is successfully.
Close the browser.
Sign back in.
Code doesn't hit OnAuthorizationCodeReceived().
Call the Microsoft.Graph
Error, IAccount is null (no token found in cache). I expected to get the token from cache
Sign-in again.
Code hit the OnAuthorizationCodeReceived().
The code I am using:
Startup.cs:
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification context)
{
IConfidentialClientApplication clientApp = MsalAppBuilder.BuildConfidentialClientApplication();
AuthenticationResult result = await clientApp.AcquireTokenByAuthorizationCode(new[] { "User.Read" }, context.Code)
.ExecuteAsync();
}
Class to store token in cache
public static class MsalAppBuilder
{
public static string GetAccountId(this ClaimsPrincipal claimsPrincipal)
{
string oid = claimsPrincipal.GetObjectId();
string tid = claimsPrincipal.GetTenantId();
return $"{oid}.{tid}";
}
private static IConfidentialClientApplication clientapp;
public static IConfidentialClientApplication BuildConfidentialClientApplication()
{
if (clientapp == null)
{
clientapp = ConfidentialClientApplicationBuilder.Create(Globals.clientId)
.WithClientSecret(Globals.clientSecret)
.WithRedirectUri(Globals.redirectUri)
.WithAuthority(new Uri(Globals.authority))
.Build();
// In-memory distributed token cache
clientapp.AddDistributedTokenCache(services =>
{
services.AddDistributedMemoryCache();
services.Configure<MsalDistributedTokenCacheAdapterOptions>(o =>
{
o.Encrypt = true;
});
});
}
return clientapp;
}
}
public static string GetData()
{
IConfidentialClientApplication app = MsalAppBuilder.BuildConfidentialClientApplication();
AuthenticationResult result = null;
var account = app.GetAccountAsync(ClaimsPrincipal.Current.GetAccountId()).Result;
string[] scopes = { "User.Read" };
try
{
// try to get an already cached token
result = app.AcquireTokenSilent(scopes, account).ExecuteAsync().Result;// ConfigureAwait(false);
//some functionality here
}
catch (Exception ex)//MsalUiRequiredException
{
return "error";
}
}
You should clear the token cache because there may have been a cache not Encrypt before
Or you adjust this code
services.Configure<MsalDistributedTokenCacheAdapterOptions>(o =>
{
o.Encrypt = false;
});

How to Unit test DefaultAzureCredential Method

I am getting the access token using the default azure credentials method while using the managed identity of the function app to get the access token.I am able to get the token. but now I am not sure how I will unit test that method. Here is the current state
private async Task RefreshTokenCache()
{
var tokenCredential = new DefaultAzureCredential();
var accessToken = await tokenCredential.GetTokenAsync(
new TokenRequestContext(scopes: new string[] { _config[AppConstants.ADAPIAppId] + "/.default" }) { });
accessTokenCache.Set(AppConstants.AccessToken, accessToken.Token, DateTimeOffset.Now.AddMinutes(55));
}
Is there anything like httpclientfactory where I can inject or I can pass some connectionstring so that I tell DefaultAzureCredential not to make the call to Azure?
update
I am adding more context. I am using this to fetch the access token in my function app from azure to authenticate itself to the APIM. so I am using a HttpMessageHandler in that I am checking If the token doesnt exist make a call to Azure and get the token.
Startup in Function App.
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddHttpClient(AppConstants.ReplyService)
//Adding token handler middleware to add authentication related details.
.AddHttpMessageHandler<AccessTokenHandler>();
}
Handler File:
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// Use the token to make the call.
// other details
request.Headers.Authorization = new AuthenticationHeaderValue(AppConstants.AuthHeader, await GetToken());
return await base.SendAsync(request, cancellationToken);
}
private async Task<string> GetToken(bool needsRefresh = false)
{
if (accessTokenCache.GetCount() == 0 || needsRefresh)
{
var tokenCredential = new DefaultAzureCredential();
var accessToken = await tokenCredential.GetTokenAsync(
new TokenRequestContext(scopes: new string[] { _config["AppId"] + "/.default" }) { });
accessTokenCache.Set("accessToken", accessToken.Token, DateTimeOffset.Now.AddMinutes(55));
}
return accessTokenCache.Get("accessToken")?.ToString() ?? throw new Exception("Unable to Fetch Access Token from Cache");
}
You shouldn't be using DefaultAzureCredential like this. It needs to be injected into service as part of the DI layer, for example here I am setting up a new BlobServiceClient:
private static void addAzureClients(IFunctionsHostBuilder builder)
{
builder.Services.AddAzureClients(builder =>
{
try
{
builder.AddBlobServiceClient(Environment.GetEnvironmentVariable("AzureWebJobsStorage"));
builder.UseCredential(new DefaultAzureCredential());
}
catch(Exception ex)
{
throw new Exception($"Error loading AddBlobServiceClient: {Environment.GetEnvironmentVariable("AzureWebJobsStorage")}", ex);
}
});
}
I then consume the client using dependancy Injection:
public MyClass(BlobServiceClient blobServiceClient)
{
this.blobServiceClient = blobServiceClient;
}
and I never have to touch the DefaultAzureCredential class at all. Then when unit testing I mock the BlobServiceClient which is an abstract class.
If you really sure you want to actually use DefaultAzureCredential then your answer is still dependency injection. I'd suggest you set it up thus:
In your startup:
builder.Services.AddSingleton<TokenCredential>(() => new DefaultAzureCredential());
Then (much like above) inject this into your class:
public MyClass(TokenCredential tokenCredential)
{
this.tokenCredential= tokenCredential;
}
Then in your test you mock TokenCredential. You cannot mock a class that you new. So you need to now do that

ADAL.NET ID-Token from logged in user

I have an webapplication which uses the Microsoft Graph API to get data from Office365 services. For the login i took the code from Microsofts sample project, which uses ADAL.NET Library for authentification.
When i make an http request it checks if the a request was authentificated. The problem is, when a new session was started no authetification request was performed, although the user is logged in. I get the error message Error Failed to acquire token silently as no token was found in the cache. Call method AcquireToken.
After research i found out that i have to call the Method AcquireTokenSilentlyAsync(), which verifies if an acceptable token is in the cache. I have implemented this method but it always throws an exception. After debugging i saw that there is no ID-Token when a logged in user makes an request. How can i get this ID-Token?
public static AuthProvider Instance { get; } = new AuthProvider();
// Get an access token. First tries to get the token from the token cache.
public async Task<string> GetUserAccessTokenAsync()
{
string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
HttpContextBase httpContextBase = HttpContext.Current.GetOwinContext().Environment["System.Web.HttpContextBase"] as HttpContextBase;
SessionTokenCache tokenCache = new SessionTokenCache(signedInUserID, httpContextBase);
var cachedItems = tokenCache.ReadItems(); // see what's in the cache
AuthenticationContext authContext = new AuthenticationContext(SettingsHelper.Authority, tokenCache);
ClientCredential clientCredential = new ClientCredential(SettingsHelper.ClientId, SettingsHelper.ClientSecret);
string userObjectId = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
UserIdentifier userId = new UserIdentifier(userObjectId, UserIdentifierType.UniqueId);
try
{
AuthenticationResult result = await authContext.AcquireTokenSilentAsync(SettingsHelper.GraphResourceId, clientCredential, userId);
return result.AccessToken;
}
// Unable to retrieve the access token silently.
catch (AdalException ex)
{
HttpContext.Current.Request.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties() { RedirectUri = "/" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
throw new Exception("Error" + $" {ex.Message}");
}
}
public class SessionTokenCache : TokenCache
{
private HttpContextBase context;
private static readonly object FileLock = new object();
private readonly string CacheId = string.Empty;
public string UserObjectId = string.Empty;
public SessionTokenCache(string userId, HttpContextBase context)
{
this.context = context;
this.UserObjectId = userId;
this.CacheId = UserObjectId + "_TokenCache";
AfterAccess = AfterAccessNotification;
BeforeAccess = BeforeAccessNotification;
Load();
}
public void Load()
{
lock (FileLock)
{
Deserialize((byte[])context.Session[CacheId]);
}
}
public void Persist()
{
lock (FileLock)
{
// Reflect changes in the persistent store.
var bytes = Serialize();
var x = System.Text.Encoding.UTF8.GetString(bytes);
context.Session[CacheId] = Serialize();
// After the write operation takes place, restore the HasStateChanged bit to false.
HasStateChanged = false;
}
}
Please go through the sample which helps in how to use MSAL.NET (Microsoft Authentication Library) to obtain an access token and will fix your issue.
Note:- MSAL is recommended since ADAL is going to be depreciated

Google login not working after publish

I use VS2015, C#.
I have a problem with Google login. From my debug configuration (localhost) everything works fine. After publishing to the server, google login window simply doesn't get opened. And no exception is thrown.
Here is my code:
[AllowAnonymous]
public async Task LoginWithGoogle()
{
HttpRequest request = System.Web.HttpContext.Current.Request;
string redirectUri = ConfigurationReaderHelper.GetGoogleRedirectUri();
try
{
ClientSecrets secrets = new ClientSecrets
{
ClientId = "***",
ClientSecret = "***"
};
IEnumerable<string> scopes = new[] { PlusService.Scope.UserinfoEmail, PlusService.Scope.UserinfoProfile };
GoogleStorageCredentials storage = new GoogleStorageCredentials();
dsAuthorizationBroker.RedirectUri = redirectUri;
UserCredential credential = await dsAuthorizationBroker.AuthorizeAsync(secrets,
scopes, "", CancellationToken.None, storage);
}
catch(Exception ex)
{
throw ex;
}
}
//just getting value from applicationSettings - web.config
public static string GetGoogleRedirectUri()
{
#if DEBUG
return GetValueFromApplicationSettings("RedirectUriDEBUG");
#elif PRODUKCIJA
return GetValueFromApplicationSettings("RedirectUriSERVER");
#endif
}
Of course I added server's address to the origin uri and also to the authorised redirect uri on the google console for developers. (just like I did for the localhost). I just don't get it what is wrong, why login windows doesn't get opened?
EDIT:
Adding class dsAuthorizationBroker (was missing from my first post - sorry on that one):
namespace Notes
{
public class dsAuthorizationBroker : GoogleWebAuthorizationBroker
{
public static string RedirectUri;
public static async Task<UserCredential> AuthorizeAsync(
ClientSecrets clientSecrets,
IEnumerable<string> scopes,
string user,
CancellationToken taskCancellationToken,
IDataStore dataStore = null)
{
var initializer = new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = clientSecrets,
};
return await AuthorizeAsyncCore(initializer, scopes, user,
taskCancellationToken, dataStore).ConfigureAwait(false);
}
private static async Task<UserCredential> AuthorizeAsyncCore(
GoogleAuthorizationCodeFlow.Initializer initializer,
IEnumerable<string> scopes,
string user,
CancellationToken taskCancellationToken,
IDataStore dataStore)
{
initializer.Scopes = scopes;
initializer.DataStore = dataStore ?? new FileDataStore(Folder);
var flow = new dsAuthorizationCodeFlow(initializer);
return await new AuthorizationCodeInstalledApp(flow,
new LocalServerCodeReceiver())
.AuthorizeAsync(user, taskCancellationToken).ConfigureAwait(false);
}
}
public class dsAuthorizationCodeFlow : GoogleAuthorizationCodeFlow
{
public dsAuthorizationCodeFlow(Initializer initializer)
: base(initializer) { }
public override AuthorizationCodeRequestUrl
CreateAuthorizationCodeRequest(string redirectUri)
{
return base.CreateAuthorizationCodeRequest(dsAuthorizationBroker.RedirectUri);
}
}
}
public static async Task<UserCredential> AuthorizeAsync
This method is already declared in GoogleWebAuthorizationBroker and therefore if you intend for your implementation of this function to take precedence over the base implementation, then you need to use the new keyword.
public new static async Task<UserCredential> AuthorizeAsync
This is why I assume you logging stops the line before
UserCredential credential = await dsAuthorizationBroker.AuthorizeAsync
At this point, it is calling the base implementation.
Aside from this, I generally tend to use DotNetOpenAuth for interacting with Google and there are plenty of simple examples to follow, like here and here.. but if you really want to roll your own using Google Apis only then this is best place to start

How to create ServiceClientCredential to be used with Microsoft.Azure.Management.Compute

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.
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;
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.
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
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" />

Categories