Home > Net >  Make dependency calls from dependency injection configuration
Make dependency calls from dependency injection configuration

Time:10-14

My need is to get a HttpClient injected and ready for use right away. But the caveat is that HttpClient needs to have Authorization header set, and for that I need to make one more call, getting the token. I managed to do all that configuration in the startup's RegisterServices, but I doubt if it is a good idea.

services.AddHttpClient("OidcClient", (isp, client) =>
{
    var options = isp.GetRequiredService<IOptions<MyConfig>>().Value;
    client.BaseAddress = new Uri(options.OidcUrl);
});

services.AddHttpClient("MyClient", (isp, client) =>
{
    var options = isp.GetRequiredService<IOptions<MyConfig>>().Value;
    var oidcClient = isp.GetRequiredService<IHttpClientFactory>().CreateClient("OidcClient");
    var data = new Dictionary<string, string>
    {
        {"client_id", options.ClientId},
        {"client_secret", options.ClientSecret}
    };

    var request = new HttpRequestMessage(HttpMethod.Post, "/connect/token") { Content = new FormUrlEncodedContent(data) };
    var response = oidcClient.SendAsync(request).Result;

    var token = response.Content.ReadFromJsonAsync<TokenResponse>().Result;

    client.BaseAddress = new Uri(options.Url);
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.Token);
});

Then it is properly injected into my code and I'm able to use the client with the authorization header.

So, my concerns are:

  • is it normal to make HTTP calls within dependency configuration?
  • haven't found anything better than .Result for an asynchronous call, is there an alternative?

And the main question: is it a 'good idea' to make dependency calls inside DI configuration?

CodePudding user response:

Is it normal[/ a 'good idea'] to make HTTP calls within dependency configuration?

No, this is a far way from the minimum required to get your dependencies up and running, it contains implementation-specific behavior that belongs in your API-client-class.

You also don't know how long the application will run and how long the token will be valid, so code expecting to get a valid and ready-to-go HttpClient in its constructor may hold on to it as long as it desires.

Wrap your HttpClient in a third-party or your own wrapper which includes the OAuth flow. Make the authentication call when your code wants to make its first call to the API, or when the token is expired, or when you get a 401 using an existing token.

As commented by @TheGeneral, one approach for such a wrapper is to use a DelegatingHandler as explained for example in this blog post. Summary:

The setup:

// The DelegatingHandler has to be registered as a Transient Service
services.AddTransient<ProtectedApiBearerTokenHandler>();

// Register our ProtectedApi client with a DelegatingHandler
// that knows how to obtain an access_token
services.AddHttpClient<IProtectedApiClient, ProtectedApiClient>(client =>
{
    client.BaseAddress = new Uri("http://localhost:5002");
    client.DefaultRequestHeaders.Add("Accept", "application/json");
}).AddHttpMessageHandler<ProtectedApiBearerTokenHandler>();

The handler:

public class ProtectedApiBearerTokenHandler : DelegatingHandler
{
    private readonly IIdentityServerClient _identityServerClient;

    public ProtectedApiBearerTokenHandler(
        IIdentityServerClient identityServerClient)
    {
        _identityServerClient = identityServerClient 
            ?? throw new ArgumentNullException(nameof(identityServerClient));
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, 
        CancellationToken cancellationToken)
    {
        // request the access token
        var accessToken = await _identityServerClient.RequestClientCredentialsTokenAsync();

        // set the bearer token to the outgoing request
        request.SetBearerToken(accessToken);

        // Proceed calling the inner handler, that will actually send the request
        // to our protected api
        return await base.SendAsync(request, cancellationToken);
    }
}

CodePudding user response:

Although the CodeCasters answer is very correct. Ideally you would use a delegating handler for this task.

It allows you to intercept the request at various stages and add whatever artefacts are needed in the request, in your case authorisation attributes, It is also async friendly.

However take care of errors and what you return to caller. lest there be no surprises for a consumer

CodePudding user response:

While @CodeCaster helped me to understand that token acquisition should not be a part of configuration, @TheGeneral hinted to use DelegatingHandler, which looks to me as ideal solution to this. Finally, I came to this code in the configuration:

services.AddTransient<AuthorizationHandler>();
services.AddHttpClient<AuthorizationHandler>((isp, client) =>
{
    var options = isp.GetRequiredService<IOptions<MyConfig>>().Value;
    client.BaseAddress = new Uri(options.OidcUrl);
});
services.AddHttpClient("MyClient", (isp, client) =>
{
    var options = isp.GetRequiredService<IOptions<MyConfig>>().Value;
    client.BaseAddress = new Uri(options.Url);
}).AddHttpMessageHandler<AuthorizationHandler>();

and a handler itself:

public class AuthorizationHandler : DelegatingHandler
{
    private readonly HttpClient _client;
    private readonly MyConfig _options;

    public AuthorizationHandler(HttpClient client, IOptions<MyConfig> options)
    {
        _client = client;
        _options = options.Value;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var token = await GetToken();

        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        return await base.SendAsync(request, cancellationToken);
    }
    ...
  • Related