AddHttpClient with HttpClientHandler, 401 unauthorized C# - c#

I try to use service.AddHttpClient to create HttpClient and DI into my other services, but HttpClient need to be set credential informations.
Here is my step,
Startup.cs
services.AddHttpClient("ServiceOne", x => {})
.ConfigurePrimaryHttpMessageHandler(() =>
{ var handler = new HttpClientHandler(){
UseDefaultCredentials = true,
Credentials = new NetworkCredential("account", "password")
};
return handler;
});
2.ServiceOne.cs
public class ServiceOne : IService
{
private readonly IHttpClientFactory _clientFactory;
}
public ServiceOne (IHttpClientFactory clientFactory)
{
this._clientFactory = clientFactory;
}
public string SomeFunction ()
{
var request = new HttpRequestMessage(HttpMethod.Get,"http://uri");
var client = _clientFactory.CreateClient("ServiceOne");
var response = client.SendAsync(request).GetAwaiter().GetResult();
}
I received 401 Unauthorized, it seems the HttpClientHandler not be set correct.
Hope someone could help me this question, Thanks.

Related

Blazor WASM Client App: Unable to read Location Response Header from HTTP Client

The Issue
I'm running into an issue where I'm unable to access the 'Location' response header (or really, any response header other than 'Content-Length') from the response of an HTTP POST, using an HTTP Client inside of a Blazor Web App.
I've confirmed that the API we are calling is indeed returning the Location header; Captured via the developer tools within Chrome:
The Code in my Blazor Client Application
We don't do anything out of the ordinary when we create the Typed/HTTP Client and register it with DI; we're just setting up the timeout on the client and Polly Retries. The handlers we're registering are for Authentication and logging respectively.
public static IServiceCollection AddInternalApiClient(this IServiceCollection services)
{
_ = services ?? throw new ArgumentNullException(nameof(services));
services.AddOptions<InternalApiClientOptions>().Configure<IConfiguration>((options, configuration) => configuration.GetSection(nameof(InternalApiClientOptions)).Bind(options));
services
.AddHttpClient<InternalApiClient>((serviceProvider, client) =>
{
var config = serviceProvider.GetRequiredService<IOptions<InternalApiClientOptions>>().Value;
client.BaseAddress = new Uri(config.BaseAddress, UriKind.Absolute);
client.Timeout = config.Timeout;
})
.AddHttpMessageHandler<ApiAuthorizationMessageHandler>()
.AddHttpMessageHandler<ApiMetricsHandler>()
.AddPolicyHandler((serviceProvider, _) =>
{
var config = serviceProvider.GetRequiredService<IOptions<InternalApiClientOptions>>().Value;
return HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(config.Retries, retryAttempt => TimeSpan.FromSeconds(Math.Pow(config.Backoff, retryAttempt)));
});
return services;
}
public class ApiAuthorizationMessageHandler : AuthorizationMessageHandler
{
public ApiAuthorizationMessageHandler(
IAccessTokenProvider provider,
NavigationManager navigationManager,
string[] authorizedUrlArray,
string scope)
: base(provider, navigationManager)
{
ConfigureHandler(
authorizedUrls: authorizedUrlArray,
scopes: new[] { scope });
}
}
public class ApiMetricsHandler : DelegatingHandler
{
private readonly SessionManager _sessionManager;
private readonly AppVersionInfo _appVersionInfo;
private readonly IMetricsMonitor _metricsMonitor;
public ApiMetricsHandler(SessionManager sessionManager, AppVersionInfo appVersionInfo, IMetricsMonitor metricsMonitor)
{
_sessionManager = sessionManager;
_appVersionInfo = appVersionInfo;
_metricsMonitor = metricsMonitor;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Dictionary<string, object> transactionDetails = new Dictionary<string, object>()
{
{ "SessionId", _sessionManager.SessionId },
// Etc.
};
_metricsMonitor.TrackEvent(_sessionManager.CurrentTransactionId, transactionDetails);
return await base.SendAsync(request, cancellationToken);
}
}
As for the Typed Client itself, its a standard NSwag-generated client. Below is a HEAVILY edited version for brevity.
public partial class InternalApiClient
{
private HttpClient _httpClient;
private Lazy<JsonSerializerSettings> _settings;
public InternalApiClient(HttpClient httpClient)
{
_httpClient = httpClient;
_settings = new Lazy<JsonSerializerSettings>(() =>
{
var settings = new JsonSerializerSettings();
UpdateJsonSerializerSettings(settings);
return settings;
});
}
protected JsonSerializerSettings JsonSerializerSettings { get { return _settings.Value; } }
// FWIW, these aren't defined elsewhere.
partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings);
partial void PrepareRequest(HttpClient client, HttpRequestMessage request, string url);
partial void PrepareRequest(HttpClient client, HttpRequestMessage request, StringBuilder urlBuilder);
partial void ProcessResponse(HttpClient client, HttpResponseMessage response);
public async Task<SwaggerResponse> PostAsync(Request body, CancellationToken cancellationToken)
{
var urlBuilder_ = new StringBuilder();
urlBuilder_.Append("endpoint/path");
var client_ = _httpClient;
try
{
using (var request_ = new HttpRequestMessage())
{
var content_ = new StringContent(JsonConvert.SerializeObject(body, _settings.Value));
content_.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
request_.Content = content_;
request_.Method = new HttpMethod("POST");
PrepareRequest(client_, request_, urlBuilder_);
var url_ = urlBuilder_.ToString();
request_.RequestUri = new System.Uri(url_, UriKind.RelativeOrAbsolute);
PrepareRequest(client_, request_, url_);
var response_ = await client_.SendAsync(request_, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
try
{
var headers_ = Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
if (response_.Content != null && response_.Content.Headers != null)
{
foreach (var item_ in response_.Content.Headers)
headers_[item_.Key] = item_.Value;
}
ProcessResponse(client_, response_);
var status_ = ((int)response_.StatusCode).ToString();
if (status_ == "400")
{
var objectResponse_ = await ReadObjectResponseAsync<ProblemDetails>(response_, headers_).ConfigureAwait(false);
throw new ApiException<ProblemDetails>("bad request", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null);
}
else
if (status_ == "202")
{
return new SwaggerResponse((int)response_.StatusCode, headers_);
}
// Etc
else
if (status_ != "200" && status_ != "204")
{
var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new ApiException("The HTTP status code of the response was not expected (" + (int)response_.StatusCode + ").", (int)response_.StatusCode, responseData_, headers_, null);
}
return new SwaggerResponse((int)response_.StatusCode, headers_);
}
finally
{
if (response_ != null)
response_.Dispose();
}
}
}
finally
{
}
}
}
Does this work elsewhere?
I ran a test using a different project that uses the same generated API Client Class, in particular an Azure Functions project. When running in that project, I was able to receive all 8 response headers in the image above.
The DI setup logic is slightly different, but the HTTP Client setup poritions remain the same:
public static IServiceCollection AddInternalApiClient(this IServiceCollection services)
{
_ = services ?? throw new ArgumentNullException(nameof(services));
services.AddOptions<InternalApiClientOptions>().Configure<IConfiguration>((options, configuration) => configuration.GetSection(nameof(InternalApiClientOptions)).Bind(options));
services
.AddHttpClient<InternalApiClient>((serviceProvider, client) =>
{
var config = serviceProvider.GetRequiredService<IOptions<InternalApiClientOptions>>().Value;
client.BaseAddress = new Uri(config.BaseAddress, UriKind.Absolute);
client.Timeout = config.Timeout;
})
.AddAzureIdentityCredentials(scopeFactory: (sp, _) =>
{
var config = sp.GetRequiredService<IOptions<InternalApiClientOptions>>().Value;
return $"{config.Scope}/.default";
})
.AddPolicyHandler((serviceProvider, _) =>
{
var config = serviceProvider.GetRequiredService<IOptions<InternalApiClientOptions>>().Value;
return HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(config.Retries, retryAttempt => TimeSpan.FromSeconds(Math.Pow(config.Backoff, retryAttempt)));
});
return services;
}
Final Thoughts
Because I'm able to access the response headers when using the same API Client in a different project, it feels like the issue is either Blazor as a whole, or my usage of the API client through it (via the Message Handlers). I've seen other questions/bugs regarding the Blazor and headers:
How to Access Httpclient headers in Blazor WASM Client
HttpClient headers are empty in blazor wasm
Blazor WASM C# .NET 6 Content-Disposition is not Accessible
The responses for these all seem to be: Update the CORS policies on the Server. I'm not sure if this IS a CORS issue though, as a different project can receive all of the headers just fine (and the 'Content-Length' header is always received no matter what). The Server itself is used by other Apps than the two I mentioned, so I'm hesitant to start adding the 'AddCors/UseCors' calls that the linked issues mentioned.
Any insight, references would be greatly appreciated.

How to register HttpClient that will authenticate domain user

I have to configure HttpClient to automatically pass Windows credentials (company API that I'm calling is using Windows Authentication). So far I was creating HttpClient inside my methods like so:
var credentialsCache = new CredentialCache {{
new Uri("https://localhost"),
"NTLM",
CredentialCache.DefaultNetworkCredentials
}};
var handler = new HttpClientHandler { Credentials = credentialsCache };
var client = new HttpClient(handler);
but since this could lead to socket exhaustion I want to use factory. Therefore inside Startup.cs I would add HttpClient like so:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddHttpClient();
}
Then I would inject factory into my service via constructor:
public class MyService
{
private readonly IHttpClientFactory clientFactory;
public MyService(IHttpClientFactory clientFactory)
{
this.clientFactory = clientFactory;
}
public async Task MakeHttpRequest()
{
var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost/endpoint");
var client = clientFactory.CreateClient();
var response = await client.SendAsync(request);
// ...
}
}
How can I configure client created this way to use network credentials? Ideally I would like to configure this only once, inside ConfigureServices()
You may look at named clients and use ConfigurePrimaryHttpMessageHandler method then
services
.AddHttpClient("myService")
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler()
{
Credentials = credentialsCache
});
HttpClient in MyService should be created using the given name
...
var client = clientFactory.CreateClient("myService");
...
You can use the options pattern to set configuration for how IHttpClientFactory creates the HttpClient. You can configure these options in the following ways
Un-named options, these will apply to all instances of HttpClient that are created by the factory
services.Configure<HttpClientFactoryOptions>(options =>
{
options.HttpMessageHandlerBuilderActions.Add(builder =>
{
var credentialsCache = new CredentialCache {{
new Uri("https://localhost"),
"NTLM",
CredentialCache.DefaultNetworkCredentials
}};
builder.PrimaryHandler = new HttpClientHandler { Credentials = credentialsCache };
});
});
Named options, these only apply to instances of HttpClient where you pass the name to the CreateClient method on the factory
services.Configure<HttpClientFactoryOptions>("myclient", options =>
{
options.HttpMessageHandlerBuilderActions.Add(builder =>
{
var credentialsCache = new CredentialCache {{
new Uri("https://localhost"),
"NTLM",
CredentialCache.DefaultNetworkCredentials
}};
builder.PrimaryHandler = new HttpClientHandler { Credentials = credentialsCache };
});
});
To use the named options you would do
var client = clientFactory.CreateClient("myclient");

getting 401 unauthorize while using IdentityModel.AspNetCore -1.0.0-rc.4.1

I am trying to access a protected API using client credential flow in my asp.net core 3.1 application.
For token management I am using IdentityModel.AspNetCore -1.0.0-rc.4.1.
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient<ApiService>(client =>
{
client.BaseAddress = new Uri("http://localhost:10811/");
})
.AddClientAccessTokenHandler();
services.AddAccessTokenManagement(options =>
{
options.Client.Clients.Add("auth", new ClientCredentialsTokenRequest
{
Address = "http://localhost:10811/token",
ClientId = "client1",
ClientSecret = "Supersecret"
});
});
}
I am always getting 401 while trying to access the protected API service.
ApiService code,
public class ApiService
{
public HttpClient HttpClient;
public ApiService(HttpClient client)
{
HttpClient = client;
}
public async Task<string> GetContactsAsync()
{
var response = await HttpClient.GetAsync("http://localhost:10811/test");
response.EnsureSuccessStatusCode();
return "Done";
}
}
And here I am calling
public class MyCallService
{
private readonly IHttpClientFactory _clientFactory;
public MyCallService(IHttpClientFactory clientFactory)
{
if (clientFactory != null)
_clientFactory = clientFactory;
}
public void Call()
{
var client = _clientFactory.CreateClient();
var apiService= new ApiService(client);
await apiService.GetContactsAsync();
}
}
Is the above code setting any token, what I am missing here? Where to put Bearer token in the authorization header.
In order to send the token with any request from the httpclient , you need to inject it before and to do that you need to use AddClientAccessTokenClient method under the AddAccessTokenManagement
services.AddClientAccessTokenClient("client", configureClient: client =>
{
client.BaseAddress = new Uri("http://localhost:10811/");
});
and you need to specifiy the name of the config to use in order to create httpclient
_client = factory.CreateClient("client");
and now you can simply call
var response = await HttpClient.GetAsync("test"); //no need to specify the full URL

Using hangfire BackgroundJob.Schedule with httpclientfactory DI

I have an api which is using the httpclientfactory to create different typed httpclients using services.addhttpclient<>(). I've started to integrate hangfire with my service due to long running jobs. Everything was working fine until I tried to use Hangfires schdule method "BackgroundJob.Schedule". It starts up, schedules the task but when it tries to execute the code I get:
"Unable to resolve service for type 'System.Net.Http.HttpClient' while attempting to activate 'BackupApi.BackupApiService'."
When trying to use Enqueue method it works without problem. Kinda lost atm, all help much appreciated. My guess is that somehow when the task has scheduled the dependencies is lost when hangfire later tries to use:
using BackupApi;
var backupApiService = Activate<BackupApiService>();
await backupApiService.AdhocBackup("BlurredServername");
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient<IBackupApiService, BackupApiService>()
.ConfigurePrimaryHttpMessageHandler(handler =>
new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => { return true; }
});
services.AddHangfire(configuration => configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseSqlServerStorage(Configuration.GetConnectionString("HangfireConnection"), new SqlServerStorageOptions
{
SchemaName = "BackupApi",
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
QueuePollInterval = TimeSpan.Zero,
UseRecommendedIsolationLevel = true,
UsePageLocksOnDequeue = true,
DisableGlobalLocks = true
}));
services.Configure<ConnectionInfo>(Configuration.GetSection("ConnectionStrings"));
services.AddHttpContextAccessor();
//services.AddTransient<IBackupApiService, BackupApiService>()
var section = Configuration.GetSection("ConnectionStrings");
services.AddHttpClient<DSDClient>()
.ConfigurePrimaryHttpMessageHandler(handler =>
new HttpClientHandler
{
Credentials = new NetworkCredential(
section["Username"],
section["Password"],
"blurredomain"),
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => { return true; }
});
services.AddHangfireServer();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
//.AddViewComponentsAsServices();
}
BackupApiService.cs
public class BackupApiService : IBackupApiService
{
public HttpClient _netclient { get; }
private static IOptions<ConnectionInfo> _config;
public DSDClient _dsdclient { get; }
private readonly IHttpContextAccessor _httpContextAccessor;
public BackupApiService(IOptions<ConnectionInfo> config, HttpClient netclient, DSDClient dsdclient, IHttpContextAccessor httpContextAccessor)
{
_config = config;
_httpContextAccessor = httpContextAccessor;
_dsdclient = dsdclient;
_netclient = netclient;
_netclient.Timeout = new TimeSpan(0, 2, 30);
_netclient.DefaultRequestHeaders.Clear();
_netclient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", _config.Value.NetworkerConnectionString);
_netclient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
public async Task<string> AdhocBackup(string ServerName)
{
....
}
BackupApiV1Controller
public class BackupApiV1Controller : ControllerBase
{
private readonly IBackupApiService _backupApiService;
public BackupApiV1Controller(IBackupApiService backupApiService)
{
_backupApiService = backupApiService;
}
[HttpPost]
[Route("StartDateBackup/")]
public IActionResult StartDateBackup([FromBody] ServerSchedule serverSchedule)
{
var resp = BackgroundJob.Schedule(() => _backupApiService.AdhocBackup(serverSchedule.Servername), serverSchedule.Date);
return Ok();
}
[HttpPost]
[Route("StartAdhocBackup/")]
public IActionResult StartAdhocBackup([FromBody] Server server)
{
var resp = BackgroundJob.Enqueue(() => _backupApiService.AdhocBackup(server.Servername));
return Ok(resp);
}

How to check whether session has timed out?

We've got 20 microservices that are all making use of a client library.
This library simply provides a way for the microservice to connect to its destination: CRM Dynamics.
Within each service we have a wrapper that calls this library:
public static class Client
{
public static HttpClient Instance;
static Client()
{
Instance = new TheCrmClient<AdfsAuthenticationHelper>(MyConfigSettings).HttpClient;
}
}
Within each service we use the client like so:
//Issue request
var response = Client.Instance.SendAsync(request).Result;
After the ADFS session has timed out, we will get a 401 response.
How do we force re-authentication when receiving a 401 response?
The authentication occurs in GetHttpsClient():
public class TheCrmClient<T> where T : IAdfsAuthenticator
{
private readonly RestCRMClientConfiguration _configuration;
private IAdfsAuthenticator _authenticator;
private HttpClient _https;
public TheCrmClient(RestCRMClientConfiguration configuration)
{
_configuration = configuration;
}
public HttpClient HttpClient => GetClientHttps();
private void InitializeAuthenticator(HttpClient client)
{
_authenticator = Activator.CreateInstance(typeof(T), client) as IAdfsAuthenticator;
}
private HttpClient GetClientHttps()
{
var clientHandler = new HttpClientHandler
{
UseCookies = true,
CookieContainer = new CookieContainer(),
AllowAutoRedirect = false
};
_https = new HttpClient(clientHandler)
{
BaseAddress = _configuration.GetServiceUri(RestCRMClientConfiguration.UrlProtocolType.Https)
};
if (_authenticator == null)
InitializeAuthenticator(_https);
_authenticator.Authenticate(_configuration.GetNetworkCredential(),
_configuration.GetAuthenticationTestEndpointUrl(RestCRMClientConfiguration.UrlProtocolType.Https));
return _https;
}
}
To clarify the lifecycle of the request is:
Microservice.Controller --> Microservice.ClientWrapper --> TheCrmClientLibrary --> CrmDestination

Categories

Resources