Quote of the Day

more Quotes

Categories

Buy me a coffee

  • Home>
  • Software Development>

Enhancing ASP.NET Core/Blazor App Security and Reusability with HttpMessageHandler and Named HttpClient

Published December 24, 2023 in Software Development - 0 Comments

In this post, I share an example of how I use a HttpMessageHandler to automatically attach a bearer token when calling a web API from an ASP.NET Core/Blazor application.

Setting Up Named HttpClients for Code Reusability

HttpClient and HttpMessageHandler together allow you to improve code reusability. For instance, suppose your web app needs to call a few different web APIs, each with different base URLs. When making a request, instead of having to enter a full URL every time, you can set up a custom http client and use it in your service.

   public static WebApplicationBuilder AddHttpClients(this WebApplicationBuilder builder)
{
    // Define a default timeout duration 120 seconds
    var defaultTimeout = TimeSpan.FromSeconds(120);

    var studioApiOptions = builder.Configuration.GetSection(OptionNames.DataManagementApi).Get<StudioApiOptions>();

    // Setup named HttpClient instances
    builder.Services.AddHttpClient(HttpClientNames.DataManagementApi, client =>
    {
        client.BaseAddress = new Uri(studioApiOptions!.BaseUrl!);
        client.Timeout = defaultTimeout;
    });

    var aiApiOptions = builder.Configuration.GetSection(HttpClientNames.AIServicesApi).Get<AIServicesApiOptions>();
    builder.Services.AddHttpClient(HttpClientNames.AIServicesApi, client =>
    {
        client.BaseAddress = new Uri(aiApiOptions!.BaseUrl);
        client.Timeout = defaultTimeout;
    });

    return builder;
}

In the above example, I setup two named http clients and added them into the container so that I could use them in services. With the clients setup, I don’t have to specify the base URLs when making calls to the two web APIs. Below shows an example of how I use the clients in my service class.

Utilizing Named HttpClients in Service Class

public class DataExchangeService : IDataExchangeService
{
  private readonly IHttpClientFactory _httpClientFactory;

  public DataExchangeService(IHttpClientFactory httpClientFactory)
  {
    _httpClientFactory = httpClientFactory;
  }

  public async Task<ICollection<DocumentDto>?> GetDocuments(int dataExchangeId)
  {
    var url = $ "./v2.1/documents/{dataExchangeId}";
    using (var httpClient = DataManagementClient())
    {
      return await httpClient.GetFromJsonAsync<ICollection<DocumentDto>>(url);
    }
  }

  private HttpClient DataManagementClient()
  {
    var client = _httpClientFactory.CreateClient(HttpClientNames.DataManagementApi);
    return client;
  }

  private HttpClient AiServicesClient()
  {
    return _httpClientFactory.CreateClient(HttpClientNames.AIServicesApi);
  }

  public async Task UploadDocument(int dataExchangeId, Stream fileStream, string documentName)
  {
    var uploadUrlPath = $ "./v2.1/data_exchange/{dataExchangeId}/embed_pdf?document_name={documentName}";

      using
      var request = new HttpRequestMessage(HttpMethod.Post, uploadUrlPath)
      {
          // codes omitted for brevity 
      };

      using (var httpClient = AiServicesClient())
     {
         var response = await httpClient.SendAsync(request);
     }
      response.EnsureSuccessStatusCode();
    }
  }
}

In the above code snippets, notice how I don’t have to specify the full URLs. Also, notice how I use HttpClientFactory to create an http client by specifying the name of the client. My app needs to call out to two different web APIs, and hence I setup the two clients, named “DataManagementApi” and “AIServicesApi”. Each client has a different base URL than the other. By using named http clients, you can put the plumbing logic of configuring the clients once and reuse them throughout the app.

Handling Bearer Token Authorization

Each of the web APIs are protected and required a bearer token for access. However, notice that I don’t have to specify the logic of obtaining the bearer token in the service. That’s because I use a custom HttpMessageHandler and configure the clients to use the handler.

 public class AuthorizationHeaderMessageHandler : DelegatingHandler
 {
     private readonly ITokenAcquisition _tokenAcquisition;
     private readonly string[] _scopes;
     private readonly IUserService _userService; 

     public AuthorizationHeaderMessageHandler(ITokenAcquisition tokenAcquisition, string[] scopes, IUserService userService)
     {
         _tokenAcquisition = tokenAcquisition;
         _scopes = scopes;
         _userService = userService;
     }

     protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
     {
         await CheckAppendAccessToken(request);
         return await base.SendAsync(request, cancellationToken);
     }

     private async Task<string?> GetAccessToken()
     {
         if (_userService.IsUserAuthenticated())
         {
             try
             {
                 var authenticationResult = await _tokenAcquisition.GetAuthenticationResultForUserAsync(_scopes);
                 var accessToken = authenticationResult.AccessToken;
                 return accessToken;
             }
             catch (Exception ex)
             {
                 Console.WriteLine(ex.StackTrace);
             }
         }
         return null; 
     }

     private async Task CheckAppendAccessToken(HttpRequestMessage request)
     {
         if (_userService.IsUserAuthenticated())
         {
             var accessToken = await GetAccessToken();
             if (accessToken != null)
                 request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
         }
     }
 }

Below code snippets show how I create and configure the client to use the handler.

builder.Services.AddHttpClient(HttpClientNames.DataManagementApi, client =>
{
    client.BaseAddress = new Uri(studioApiOptions!.BaseUrl!);
    client.Timeout = defaultTimeout;
    
}).AddHttpMessageHandler( _ => new AuthorizationHeaderMessageHandler(tokenAcquisition!, studioApiOptions!.Scopes, userService!));

var aiApiOptions = builder.Configuration.GetSection(HttpClientNames.AIServicesApi).Get<AIServicesApiOptions>();
builder.Services.AddHttpClient(HttpClientNames.AIServicesApi, client =>
{
    client.BaseAddress = new Uri(aiApiOptions!.BaseUrl);
    client.Timeout = defaultTimeout;
}).AddHttpMessageHandler(_ => new AuthorizationHeaderMessageHandler(tokenAcquisition!, aiApiOptions!.Scopes, userService!));
           

Conclusion

In conclusion, leveraging named HttpClient instances and a custom HttpMessageHandler not only simplifies API calls but also enhances code reusability and maintains a clean separation of concerns. I encourage you to explore and integrate these practices into your projects, improving code organization and promoting a more secure and scalable architecture. Happy coding!

References

Make HTTP requests using IHttpClientFactory in ASP.NET Core | Microsoft Learn

ASPNETCORE Outgoing Request Middleware

HttpMessageHandler Class (System.Net.Http) | Microsoft Learn

No comments yet