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
Related
This video is really nice and shows how to create Minimal APIs using .net 6:
https://www.youtube.com/watch?v=eRJFNGIsJEo
It is amazing how it uses dependency injection to get mostly everything that you need inside your endpoints. For example if I need the value of a custom header I would have this:
app.MapGet("/get-custom-header", ([FromHeader(Name = "User-Agent")] string data) =>
{
return $"User again is: {data}";
});
I can have another endpoint where I have access to the entire httpContext like this:
app.MapGet("/foo", (Microsoft.AspNetCore.Http.HttpContext c) =>
{
var path = c.Request.Path;
return path;
});
I can even register my own classes with this code: builder.Services.AddTransient<TheClassIWantToRegister>()
If I register my custom classes I will be able to create an instance of that class every time I need it on and endpoint (app.MapGet("...)
Anyways back to the question. When a user logs in I send him this:
{
"ApiKey": "1234",
"ExpirationDate": blabla bla
.....
}
The user must send the 1234 token to use the API. How can I avoid repeating my code like this:
app.MapGet("/getCustomers", ([FromHeader(Name = "API-KEY")] string apiToken) =>
{
// validate apiToken agains DB
if(validationPasses)
return Database.Customers.ToList();
else
// return unauthorized
});
I have tried creating a custom class RequiresApiTokenKey and registering that class as builder.Services.AddTransient<RequiresApiTokenKey>() so that my API knows how to create an instance of that class when needed but how can I access the current http context inside that class for example? How can I avoid having to repeat having to check if the header API-KEY header is valid in every method that requires it?
Gave this a test based on my comments.
This would call the method Invoke in the middleware on each request and you can do checks here.
Probably a better way would be to use the AuthenticationHandler. using this would mean you can attribute individual endpoints to have the API key check done instead of all incoming requests
But, I thought this was still useful, middleware can be used for anything you'd like to perform on every request
Using Middleware
Program.cs:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
//our custom middleware extension to call UseMiddleware
app.UseAPIKeyCheckMiddleware();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.MapGet("/", () => "Hello World!");
app.Run();
APIKeyCheckMiddleware.cs
using Microsoft.Extensions.Primitives;
internal class APIKeyCheckMiddleware
{
private readonly RequestDelegate _next;
public APIKeyCheckMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
//we could inject here our database context to do checks against the db
if (httpContext.Request.Headers.TryGetValue("API-KEY", out StringValues value))
{
//do the checks on key
var apikey = value;
}
else
{
//return 403
httpContext.Response.StatusCode = 403;
}
await _next(httpContext);
}
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class APIKeyCheckMiddlewareExtensions
{
public static IApplicationBuilder UseAPIKeyCheckMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<APIKeyCheckMiddleware>();
}
}
I used SmithMart's answer but had to change things in the Invoke method and used DI in the constructor.
Here's my version:
internal class ApiKeyCheckMiddleware
{
public static string ApiKeyHeaderName = "X-ApiKey";
private readonly RequestDelegate _next;
private readonly ILogger<ApiKeyCheckMiddleware> _logger;
private readonly IApiKeyService _apiKeyService;
public ApiKeyCheckMiddleware(RequestDelegate next, ILogger<ApiKeyCheckMiddleware> logger, IApiKeyService apiKeyService)
{
_next = next;
_logger = logger;
_apiKeyService = apiKeyService;
}
public async Task InvokeAsync(HttpContext httpContext)
{
var request = httpContext.Request;
var hasApiKeyHeader = request.Headers.TryGetValue(ApiKeyHeaderName, out var apiKeyValue);
if (hasApiKeyHeader)
{
_logger.LogDebug("Found the header {ApiKeyHeader}. Starting API Key validation", ApiKeyHeaderName);
if (apiKeyValue.Count != 0 && !string.IsNullOrWhiteSpace(apiKeyValue))
{
if (Guid.TryParse(apiKeyValue, out Guid apiKey))
{
var allowed = await _apiKeyService.Validate(apiKey);
if (allowed)
{
_logger.LogDebug("Client successfully logged in with key {ApiKey}", apiKeyValue);
var apiKeyClaim = new Claim("ApiKey", apiKeyValue);
var allowedSiteIdsClaim = new Claim("SiteIds", string.Join(",", allowedSiteIds));
var principal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim> { apiKeyClaim, allowedSiteIdsClaim }, "ApiKey"));
httpContext.User = principal;
await _next(httpContext);
return;
}
}
_logger.LogWarning("Client with ApiKey {ApiKey} is not authorized", apiKeyValue);
}
else
{
_logger.LogWarning("{HeaderName} header found, but api key was null or empty", ApiKeyHeaderName);
}
}
else
{
_logger.LogWarning("No ApiKey header found.");
}
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
}
}
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.
My understanding is, I might be wrong, that with Microsoft Graph SDK, you get a GraphClientFactory and by using it you can plug in or remove middleware components from the graph client.
Since they provide a way to remove and add middleware, I assumed that we can provide a custom implementation of Authentication Handler as it is basically a delegating handler as can be seen
here.
I have written this code which is very similar to the Original provided by Microsoft Graph but trimmed down just to see if we can actually use custom components/handlers:
public class CustomAuthenticationHandler: DelegatingHandler
{
internal AuthenticationHandlerOption AuthOption { get; set; }
public IAuthenticationProvider AuthenticationProvider { get; set; }
public CustomAuthenticationHandler(IAuthenticationProvider authenticationProvider)
{
AuthenticationProvider = authenticationProvider;
AuthOption = new AuthenticationHandlerOption();
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage httpRequestMessage, CancellationToken cancellationToken)
{
var authProvider = AuthenticationProvider;
if (authProvider != null)
{
await authProvider.AuthenticateRequestAsync(httpRequestMessage);
HttpResponseMessage response = await base.SendAsync(httpRequestMessage, cancellationToken);
//do something
return response;
}
else
{
return await base.SendAsync(httpRequestMessage, cancellationToken);
}
}
}
And this is how I add this handler to pipeline:
var handlers = new DelegatingHandler[]
{
new CustomAuthenticationHandler(
new DelegateAuthenticationProvider(async (requestMessage) =>
{
var scopes = new string[] { "https://graph.microsoft.com/.default" };
var authResult = await confidentialClientApplication.AcquireTokenForClient(scopes).ExecuteAsync();
requestMessage
.Headers
.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
})
),
new CompressionHandler(),
new RetryHandler(),
new RedirectHandler()
};
var client = GraphClientFactory.Create(handlers);
GraphClient = new GraphServiceClient(client);
That's the error message I get when actually sending the request:
"Code: invalidRequest\r\nMessage: Authentication provider is required before sending a request.\r\n"
So far I haven't been able to work. My question is this possible or not? Does Microsoft Graph SDK intend to provide this functionality or I understand it completely wrong? If it is possible to add custom handlers, then what am I doing wrong?
I do not know if this is actually possible, but I think it' s worth a try to find out.
There are maybe other and better patterns (if you know one let me know, I will look them up) to do this, but I'm just curious to know if this is possible.
When you have to call an API you could do it directly from within the controller using the HttpClient like this:
[Authorize]
public async Task<IActionResult> Private()
{
//Example: get some access token to use in api call
var accessToken = await HttpContext.GetTokenAsync("access_token");
//Example: do an API call direcly using a static HttpClient wrapt in a service
var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/api/some/endpoint");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var response = await _client.Client.SendAsync(request);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
//Handle situation where user is not authenticated
var rederectUrl = "/account/login?returnUrl="+Request.Path;
return Redirect(rederectUrl);
}
if (response.StatusCode == HttpStatusCode.Forbidden)
{
//Handle situation where user is not authorized
return null;
}
var text = await response.Content.ReadAsStringAsync();
Result result = JObject.Parse(text).ToObject<Result>();
return View(result);
}
When you would do this you'll have to reuse some code over and over again. You could just make a Repository but for some scenarios that would be overkill and you just want to make some quick and dirty API calls.
Now what I want to know is, when we move the logic of setting an Authorization header or handling the 401 and 403 responses outside the controller, how do you redirect or control the controller's action.
Lets say I create a Middleware for the HttpClient like this:
public class ResourceGatewayMessageHandler : HttpClientHandler
{
private readonly IHttpContextAccessor _contextAccessor;
public ResourceGatewayMessageHandler(IHttpContextAccessor context)
{
_contextAccessor = context;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
//Retrieve acces token from token store
var accessToken = await _contextAccessor.HttpContext.GetTokenAsync("access_token");
//Add token to request
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
//Execute request
var response = await base.SendAsync(request, cancellationToken);
//When 401 user is probably not logged in any more -> redirect to login screen
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
//Handle situation where user is not authenticated
var context = _contextAccessor.HttpContext;
var rederectUrl = "/account/login?returnUrl="+context.Request.Path;
context.Response.Redirect(rederectUrl); //not working
}
//When 403 user probably does not have authorization to use endpoint
if (response.StatusCode == HttpStatusCode.Forbidden)
{
//Handle situation where user is not authorized
}
return response;
}
}
We can just do the request like this:
[Authorize]
public async Task<IActionResult> Private()
{
//Example: do an API call direcly using a static HttpClient initiated with Middleware wrapt in a service
var response = await _client.Client.GetAsync("https://example.com/api/some/endpoint");
var text = await response.Content.ReadAsStringAsync();
Result result = JObject.Parse(text).ToObject<Result>();
return View(result);
}
The problem here is that context.Response.Redirect(rederectUrl); does not work. It does not break off the flow to redirect. Is it possible to implement this, and how would you solve this?
Ok since nobody answers my question I've thought about it thoroughly and I came up with the following:
Setup
We have a resource gateway (RG). The RG can return a 401 or 403 meaning that the session is expired (401) or the user does not have sufficient rights (403). We use an access token (AT) to authenticate and authorize our requests to the RG.
authentication
When we get a 401 and we have a refresh token (RT) we want to trigger something that will retrieve a new AT. When there is no RT or the RT is expired we want to reauthenticate the user.
authorization
When we get a 403 we want to show the user that he has no access or something similar like that.
Solution
To handle the above, without making it a hassle for the programmer that uses the API or API wrapper class we can use a Middleware that will specifically handle the Exception thrown by using the API or the API wrapper. The middleware can handle any of the above situations.
Create custom Exceptions
public class ApiAuthenticationException : Exception
{
public ApiAuthenticationException()
{
}
public ApiAuthenticationException(string message) : base(message)
{
}
}
public class ApiAuthorizationException : Exception
{
public ApiAuthorizationException()
{
}
public ApiAuthorizationException(string message) : base(message)
{
}
}
Throw exceptions
Create a wrapper or use the HttpClient middleware to manage the exception throwing.
public class ResourceGatewayMessageHandler : HttpClientHandler
{
private readonly IHttpContextAccessor _contextAccessor;
public ResourceGatewayMessageHandler(IHttpContextAccessor context)
{
_contextAccessor = context;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
//Retrieve acces token from token store
var accessToken = await _contextAccessor.HttpContext.GetTokenAsync("access_token");
//Add token to request
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
//Execute request
var response = await base.SendAsync(request, cancellationToken);
//When 401 user is probably not logged in any more -> redirect to login screen
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
throw new ApiAuthenticationException();
}
//When 403 user probably does not have authorization to use endpoint -> show error page
if (response.StatusCode == HttpStatusCode.Forbidden)
{
throw new ApiAuthorizationException();
}
return response;
}
}
Now you have to setup the HttpClient inside your Startup.cs. There are multiple ways to do this. I advise to use AddTransient to innitiate a wrapper class that uses a HttpClient as a static.
You could do it like this:
public class ResourceGatewayClient : IApiClient
{
private static HttpClient _client;
public HttpClient Client => _client;
public ResourceGatewayClient(IHttpContextAccessor contextAccessor)
{
if (_client == null)
{
_client = new HttpClient(new ResourceGatewayMessageHandler(contextAccessor));
//configurate default base address
_client.BaseAddress = "https://gateway.domain.com/api";
}
}
}
And in your Startup.cs inside the ConfigureServices(IServiceCollection services) you can do:
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<ResourceGatewayClient>();
Now you can use the dependency injection in any controller you would like.
Handle the Exceptions
Create something like this middleware (with thanks to this answer):
public class ApiErrorMiddleWare
{
private readonly RequestDelegate next;
public ApiErrorMiddleWare(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
await next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
if (exception is ApiAuthenticationException)
{
context.Response.Redirect("/account/login");
}
if (exception is ApiAuthorizationException)
{
//handle not authorized
}
}
Register your middleware
Go to Startup.cs and go to the Configure(IApplicationBuilder app, IHostingEnvironment env) method and add app.UseMiddleware<ApiErrorMiddleWare>();.
This should do it. Currently, I'm creating an example when it is publicly available (after peer review) I'll add a github reference.
I would like to hear some feedback on this solution or an alternative approach.
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" />