In this article, we are going to discuss how to use basic authentication with HttpClient. While the topic may seem straightforward, there are a few different ways to solve this problem. We’ll try and cover some of the main approaches, and hopefully, by the end, we will know which approach is best and why.

To download the source code for this article, you can visit our GitHub repository.

Before we dive into the code, it’s always helpful to set some context on the problem itself.

What Is Basic Authentication?

When we are trying to protect some resources (for example, an operation on a backend API, or maybe access to some data), we require some kind of authentication to secure this access. This requires the consumer to identify themselves to the target server, to gain access to the resource it requires. It does this via the Authorization header in the HTTP Request.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

A sample Basic Authentication header might look like this:

Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=

The value comprises the word Basic (to identify the scheme), followed by a space, followed by a Base-64 encoded value of a username/password combination, in the format of username:password. 

Comparing Basic Authentication to Other Authentication Schemes

Since Base-64 is a very simple algorithm with no encryption which is easy to reverse, any attacker could easily figure out the username and password and issue that same request to gain access. Furthermore, since the credential is passed in every request, it’s very simple for an attacker to impersonate the real user. 

If we contrast this to the more popular method of OAuth, all these problems are solved. Therefore, if we are trying to secure applications over the public internet, it’s generally preferred to use OAuth or some other style of token-based authentication.

It’s important to always understand the limitations of the practices we apply, so we can make an informed judgment choice. Now that we understand those limitations, let’s move on and see how to apply basic authentication in .NET with HttpClient.

Building the Server

To demonstrate basic authentication, we require a client and server. To show this in its simplest form, we’ll create the server and client as separate ASP.NET Core APIs. But in reality, the applications can be anything that can deal with HTTP, as that’s the protocol for basic authentication security.

Let’s start with the server, by creating a standard ASP.NET Core API with Controllers, calling it “Server” and adding a method called IsRequestAuthenticated():

private bool IsRequestAuthenticated()
{
    try
    {
        if (AuthenticationHeaderValue.TryParse(Request.Headers.Authorization, 
                                               out var basicAuthCredential))
        {
            if (basicAuthCredential.Scheme == "Basic" &&
                !string.IsNullOrWhiteSpace(basicAuthCredential.Parameter))
            {
                var usernamePassword = 
                    Encoding.UTF8.GetString(Convert.FromBase64String(basicAuthCredential.Parameter));
                if (!string.IsNullOrWhiteSpace(usernamePassword))
                {
                    var separatorIndex = usernamePassword.IndexOf(':');

                    var username = usernamePassword[..separatorIndex];
                    var password = usernamePassword[(separatorIndex+ 1)..];

                    if (username == "codemaze" &&
                        password == "isthebest")
                    {
                        return true;
                    }
                }
            }
        }
    }
    catch
    {
        //logic for catching exceptions here
    }

    return false;
}

There’s a lot going on in the code, but essentially we are getting the value from the header, decoding it, and ensuring it matches the username-password combination we expect. If it does, we return true. Otherwise, false.

Now let’s modify the existing Get() method to make use of our new method:

[HttpGet]
public IActionResult Get()
{
    if (IsRequestAuthenticated())
    {
        return Ok(Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        }));
    }
    
    return Unauthorized();            
}

It’s very straightforward: if the request is authenticated we allow it through, otherwise, we return Unauthorized (401).

We should note there are much cleaner ways to this approach.

First, we should set up some middleware to run on every request so that we don’t need to repeat ourselves and keep the authentication code separate from our logic. After that, there are several NuGet packages available that can do all this heavy lifting for us and improve some of our security logic.

However, since we are just demonstrating the concept here, this code is fine.

Testing Basic Authentication on the Server

Let’s hit the endpoint in Postman with no additional headers to observe the result and ensure that our server is secured as we expect:

Server with Basic Authentication Enabled

As we expected, we got an 401 Unauthorized response.

Let’s now set the Authorization header with the value of Basic Y29kZW1hemU6aXN0aGViZXN0 (which is the Base-64 encoded representation of codemaze:isthebest):

Successful Request With Basic Authentication

This time the request was successful, and we get a 200 OK status code and the response we expected.

We have a server protected with basic authentication. Now it’s time to set up the client to communicate with it.

Building the Client

Let’s add another project to our solution, again an ASP.NET Core API, this time let’s call it “Client”. Let’s modify the WeatherForecastController:

using Microsoft.AspNetCore.Mvc;

namespace Client.Controllers;

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private static readonly HttpClient _httpClient = new HttpClient();

    public WeatherForecastController()
    {
        _httpClient.BaseAddress = new Uri("https://localhost:7003");
    }

    [HttpGet]
    public async Task<IActionResult> Get()
    {
        var response = await _httpClient.GetStringAsync("weatherforecast");

        return Content(response, "application/json");
    }
}

The most notable aspect of our code is the use of HttpClient, which is the preferred way to interact with remote services over HTTP.

As we demonstrate in our Fetching Data and Content Negotiation with HttpClient in ASP.NET Core article, we create a static instance of HttpClient, set the base address in the constructor then call out to it in our Get() method. Because we know the response is JSON (when successful), we can use the convenient GetStringAsync() method, and instead of trying to deserialize again into a model, we can return it straight back. 

Let’s see what happens if we execute our Get() route in Postman:

Failing Client Request Without Basic Authentication

The client code crashes, returning  a 500 Internal Server Error, because it received a 401 Unauthorized error. We expect this, as, of course, we didn’t set the header.

Note: Exposing the raw exception details in production environments is not the best way to deal with errors on an API. For more details on how to approach this, check out our article on the ProblemDetails class.

There are a few ways to do this, let’s start with the manual way.

Setting the Basic Authentication Header

Let’s modify our Get() method:

[HttpGet]
public async Task<IActionResult> Get()
{
    _httpClient.DefaultRequestHeaders.Clear();
    _httpClient.DefaultRequestHeaders.Add("Authorization", "Basic Y29kZW1hemU6aXN0aGViZXN0");

    var response = await _httpClient.GetStringAsync("weatherforecast");

    return Content(response, "application/json");
}

Because we know the value it needs to be, we can simply hardcode it in the Authorization header. If we run the request again, we see it succeeds and we receive the response we expect:

Successful Client Request With Basic Authentication

However, this code has several issues. If the username or password changes, we need to re-generate the Base-64 encoded value external to this app and redeploy it. Moreover, we should avoid embedding security credentials in code and source control.

Let’s look to improve these issues.

Improving the Basic Authentication Logic

Let’s first look at securing the credential. Right-click on our project and select “Manage User Secrets”, and add a couple of values:

{
  "BasicAuthenticationUsername": "codemaze",
  "BasicAuthenticationPassword": "isthebest"
}

User Secrets are a great way to keep sensitive values out of code and source control. If we wanted to deploy this somewhere, for example, Azure App Service, we could leverage something like Azure Key Vault. However, for our example here, this is perfectly secure.

Let’s modify our code:

[HttpGet]
public async Task<IActionResult> Get()
{
    var basicAuthenticationUsername = _configuration["BasicAuthenticationUsername"];
    var basicAuthenticationPassword = _configuration["BasicAuthenticationPassword"];
    var basicAuthenticationValue = 
        Convert.ToBase64String(
            Encoding.ASCII.GetBytes($"{basicAuthenticationUsername}:{basicAuthenticationPassword}"));

    _httpClient.DefaultRequestHeaders.Clear();
    _httpClient.DefaultRequestHeaders.Add("Authorization", $"Basic {basicAuthenticationValue}");

    var response = await _httpClient.GetStringAsync("weatherforecast");
    
    return Content(response, "application/json");
}

This time we are reading the values from the configuration, and applying them at runtime. This means if the values change, all we need to do is update the secrets and restart the app. No redeployment is necessary. Note we could also use a strongly-typed configuration instead of dealing with IConfiguration directly which is the preferred way, but that is outside the scope of this article.

We can simplify our code that sets the header even further:

[HttpGet]
public async Task<IActionResult> Get()
{
    var basicAuthenticationUsername = _configuration["BasicAuthenticationUsername"];
    var basicAuthenticationPassword = _configuration["BasicAuthenticationPassword"];
    var basicAuthenticationValue = 
        Convert.ToBase64String(
            Encoding.ASCII.GetBytes($"{basicAuthenticationUsername}:{basicAuthenticationPassword}"));

    _httpClient.DefaultRequestHeaders.Authorization = 
        new AuthenticationHeaderValue("Basic", basicAuthenticationValue);

    var response = await _httpClient.GetStringAsync("weatherforecast");

    return Content(response, "application/json");
}

This is cleaner as we don’t need to clear the default headers or perform string manipulation in the value.

Everything is working great!

However there is one more problem with our code, and that is the use of HttpClient itself. In the next section let’s look at why.

Correct Use of HttpClient

In our code, we are creating a static instance of HttpClient. This exposes several issues, including DNS caching issues, repeating configuration code and incorrectly disposing of resources to name a few. It’s tempting to make use of using statements to combat this problem, but then we will create even more HttpClient instances and run into socket exhaustion issues.

There is a better way that solves all these problems, and that is with the use of HttpClientFactory. We cover this in detail in our Using HttpClientFactory in ASP.NET Core Applications article, so let’s jump right into how to apply it here.

First, let’s create a class WeatherForecastClient:

using System.Net.Http.Headers;
using System.Text;

namespace Client;

public class WeatherForecastClient
{
    private readonly HttpClient _httpClient;

    public WeatherForecastClient(HttpClient httpClient, IConfiguration configuration)
    {
        _httpClient = httpClient;
        _httpClient.BaseAddress = new Uri("https://localhost:7003");
        var basicAuthenticationUsername = configuration["BasicAuthenticationUsername"];
        var basicAuthenticationPassword = configuration["BasicAuthenticationPassword"];
        var basicAuthenticationValue = 
            Convert.ToBase64String(
                Encoding.ASCII.GetBytes($"{basicAuthenticationUsername}:{basicAuthenticationPassword}"));
        _httpClient.DefaultRequestHeaders.Authorization = 
            new AuthenticationHeaderValue("Basic", basicAuthenticationValue);
    }

    public async Task<string> GetAsync() => await _httpClient.GetStringAsync("weatherforecast");
}

This is all the logic we previously had in our Get() method. 

Let’s add a line to our Program.cs:

builder.Services.AddHttpClient<WeatherForecastClient>();

This will use HttpClientFactory behind the scenes, and ensure instances are pooled where required.

Now our WeatherForecastController is much cleaner:

using Microsoft.AspNetCore.Mvc;

namespace Client.Controllers;

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private readonly WeatherForecastClient _client;
    public WeatherForecastController(WeatherForecastClient client)
    {
        _client = client;
    }

    [HttpGet]
    public async Task<IActionResult> Get()
    {
        var response = await _client.GetAsync();

        return Content(response, "application/json");
    }
}

All our HttpClient configuration is now external to our logic, which means our controller can focus on the inputs and outputs, and not the HTTP semantics.

Conclusion

In this article, we demonstrate how to secure a server with Basic Authentication and configure a client to pass the appropriate credentials. Hopefully, with the approach shown and the modern usage of HttpClient, you are well-equipped to build scalable and secure solutions for your use case.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!