Securing an ASP.NET Core app and web API using windows authentication

This post shows how an ASP.NET Core Web API and an ASP.NET Core Razor page application can be implemented to use windows authentication. The Razor page application uses Javascript to display an autocomplete control which gets the data indirectly from the service API which is protected using windows authentication. The Razor Page application uses the API to get the auto-complete suggestions data. Both applications are protected using windows authentication.

Code: https://github.com/damienbod/PoCWindowsAuth

Setup the API

The ASP.NET Core demo API is setup to use windows authentication. The launch settings windowsAuthentication property is set to true and the anonymousAuthentication property to false. The application host file settings on your development PC would also need to be configured to allow windows authentication, which is disabled by default. See the stack overflow link at the bottom for more information.

{
  "iisSettings": {
    "windowsAuthentication": true,
    "anonymousAuthentication": false,
    "iisExpress": {
      "applicationUrl": "https://localhost:44364",
      "sslPort": 44364
    }
  },

The Startup ConfigureServices method is configured to require authentication using the IISDefaults.AuthenticationScheme scheme. This would need to be changed if you were using a different hosting model.

public void ConfigureServices(IServiceCollection services)
{
	services.AddAuthentication(IISDefaults.AuthenticationScheme);

	services.AddControllers().AddJsonOptions(option =>
		option.JsonSerializerOptions
              .PropertyNamingPolicy = JsonNamingPolicy.CamelCase);
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
	}

	app.UseHttpsRedirection();

	app.UseRouting();

	app.UseAuthentication();
	app.UseAuthorization();

	app.UseEndpoints(endpoints =>
	{
		endpoints.MapControllers();
	});
}

The API is protected using the authorize attribute. This example returns the user name from the windows authentication.

[Authorize]
[ApiController]
[Route("api/[controller]")]
public class MyDataController : ControllerBase
{

	private readonly ILogger<MyDataController> _logger;

	public MyDataController(ILogger<MyDataController> logger)
	{
		_logger = logger;
	}

	[HttpGet]
	public IEnumerable<string> Get()
	{
		return new List<string> { User.Identity.Name };
	}
}

Implement the ASP.NET Core Razor pages

The application calling the API also requires windows authentication and requests the data from the API project. The HttpClient instance requesting the data from the API project must send the default credentials with each API call. A HttpClientHandler is used to implement this. The HttpClientHandler is added to a named AddHttpClient service which can be used anywhere in the application.

public void ConfigureServices(IServiceCollection services)
{
	services.AddAuthentication(IISDefaults.AuthenticationScheme);

	services.AddHttpClient();

	HttpClientHandler handler = new HttpClientHandler()
	{
		UseDefaultCredentials = true
	};

	services.AddHttpClient("windowsAuthClient", c =>{ })
		.ConfigurePrimaryHttpMessageHandler(() => handler);

	services.AddScoped<MyDataClientService>();
	services.AddRazorPages().AddJsonOptions(option =>
		option.JsonSerializerOptions
			.PropertyNamingPolicy = JsonNamingPolicy.CamelCase);
}

A client service is implemented to call the API from the second project. This client uses the IHttpClientFactory to create instances of the HttpClient. The CreateClient method is used to create an instance using the named client which was configured in the Startup class. This instance will send credentials to the API.

public MyDataClientService(
	IConfiguration configurations,
	IHttpClientFactory clientFactory)
{
	_configurations = configurations;
	_clientFactory = clientFactory;
	_jsonSerializerOptions = new JsonSerializerOptions
	{
		PropertyNameCaseInsensitive = true,
	};
}

public async Task<List<string>> GetMyData()
{
	try
	{
		var client = _clientFactory.CreateClient("windowsAuthClient");
		client.BaseAddress = new Uri(_configurations["MyApiUrl"]);

		var response = await client.GetAsync("api/MyData");
		if (response.IsSuccessStatusCode)
		{
			var data = await JsonSerializer.DeserializeAsync<List<string>>(
			await response.Content.ReadAsStreamAsync());

			return data;
		}

		var error = await response.Content.ReadAsStringAsync();
		throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}, Message: {error}");

	}
	catch (Exception e)
	{
		throw new ApplicationException($"Exception {e}");
	}
}

Javascript UI

If using Javascript to call the API protected with window authentication, this can become a bit tricky due to CORS when using windows authentication. I prefer to avoid this and use a backend to proxy the calls from my trusted backend to the API. The OnGetAutoCompleteSuggest method is used to call the API. The would also make it easy to map DTOs from my API to my view DTOs as required.

 public class IndexModel : PageModel
    {
        private readonly ILogger<IndexModel> _logger;
        private readonly MyDataClientService _myDataClientService;
        public List<string> DataFromApi;
        public string SearchText { get; set; }
        public List<PersonCity> PersonCities;

        public IndexModel(MyDataClientService myDataClientService,
            ILogger<IndexModel> logger)
        {
            _myDataClientService = myDataClientService;
            _logger = logger;
        }

        public async Task OnGetAsync()
        {
            DataFromApi = await _myDataClientService.GetMyData();
        }

        public async Task<ActionResult> OnGetAutoCompleteSuggest(string term)
        {
            PersonCities = await _myDataClientService.Suggest(term);
            SearchText = term;

            return new JsonResult(PersonCities);
        }
    }

The Razor Page underneath uses an autocomplete implemented in Javascript to suggest data requested from the API. Any Javascript framework can be used in this way.

@page "{handler?}"
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

<div class="text-center">
    <p>Data from API:</p>

    @foreach (string item in Model.DataFromApi)
    {
        <p>@item</p><br />
    }
</div>

<hr />

<fieldset class="form">
    <legend>Search for a person in the search engine</legend>
    <table width="500">
        <tr>
            <th></th>
        </tr>
        <tr>
            <td>
                <input class="form-control" id="autocomplete" type="text" style="width:500px" />
            </td>
        </tr>
    </table>
</fieldset>

<br />


<div class="card" id="results">
    <h5 class="card-header">
        <span id="docName"></span>
        <span id="docFamilyName"></span>
    </h5>
    <div class="card-body">
        <p class="card-text"><span id="docInfo"></span></p>
        <p class="card-text"><span id="docCityCountry"></span></p>
        <p class="card-text"><span id="docWeb"></span></p>
    </div>
</div>

@section scripts
{
    <script type="text/javascript">
        var items;
        $(document).ready(function () {
            $("#results").hide();
            $("input#autocomplete").autocomplete({
                source: function (request, response) {
                    $.ajax({
                        url: "Index/AutoCompleteSuggest",
                        dataType: "json",
                        data: {
                            term: request.term,
                        },
                        success: function (data) {
                            var itemArray = new Array();
                            for (i = 0; i < data.length; i++) {
                                itemArray[i] = {
                                    label: data[i].name + " " + data[i].familyName,
                                    value: data[i].name + " " + data[i].familyName,
                                    data: data[i]
                                }
                            }

                            console.log(itemArray);
                            response(itemArray);
                        },
                        error: function (data, type) {
                            console.log(type);
                        }
                    });
                },
                select: function (event, ui) {
                    $("#results").show();
                    $("#docNameId").text(ui.item.data.id);
                    $("#docName").text(ui.item.data.name);
                    $("#docFamilyName").text(ui.item.data.familyName);
                    $("#docInfo").text(ui.item.data.info);
                    $("#docCityCountry").text(ui.item.data.cityCountry);
                    $("#docWeb").text(ui.item.data.web);
                    console.log(ui.item);
                }
            });
        });
    </script>
}

If all is setup correctly, the ASP.NET Core application displays the API data which is protected using the windows authentication.

CRSF

If using windows authentication, you need to protect against CSRF forgery like any application using cookies. It is also recommended NOT to use windows authentication in the public domain. Modern security architectures should be used like Open ID Connect whenever possible. This works well on intranets or for making changes to existing applications which use windows authentication in secure networks.

Links:

https://stackoverflow.com/questions/36946304/using-windows-authentication-in-asp-net

https://docs.microsoft.com/en-us/aspnet/web-api/overview/security/integrated-windows-authentication

2 comments

  1. […] Securing an ASP.NET Core app and web API using windows authentication (Damien Bowden) […]

  2. […] Securing an ASP.NET Core app and web API using windows authentication – Damien Bowden […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: