Quantcast
Channel: ASP.NET Core – Software Engineering
Viewing all 169 articles
Browse latest View live

Using MVC ASP.NET Core APPs in a Host ASP.NET Core Application

$
0
0

This article shows how ASP.NET Core applications could be deployed inside a separate host ASP.NET Core MVC application. This could be useful if you have separate applications, services, or layouts but want to have a common user interface, or common deployment to improve the user experience.

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

Setup

The applications are split into four different projects to demonstrate some of the possibilities. Two applications are complete ASP.NET Core MVC applications with a database, separate services, use localization and a unique layout.

The host application references the child MVC applications and the shared project contains the localizations for all applications.

The projects can be added as references in visual studio.

ASP.NET Core Areas

The MVC applications are referenced into the host application and use areas per application. This needs to be added to the route configuration. The areas route is added first, and then the default routes for the host application views and controllers.

Host application routing:

app.UseMvc(routes =>
{
	routes.MapRoute(
	  name: "areas",
	  template: "{area:exists}/{controller=Home}/{action=Index}/{id?}"
	);

	routes.MapRoute(
		name: "default",
		template: "{controller=Home}/{action=Index}/{id?}");
});

In the child applications, the UI logic is added to an area. The area attribute MUST be added to the MVC controller. If your area controller, view is not working, this is usually the problem.

Example of a controller in one of the child applications:

namespace MvcApp1.Controllers
{
    [Area("MvcApp1")]
    public class HomeController : Controller
    {

Area setup solution explorer:

ASP.NET Core Layouts

Each application uses its own layout. This is defined in the _ViewStart.cshtml file. This makes it really easy to have application specific layouts, even when hosting inside the separate application.

One problem with this, is that all applications use the same css and js files, ie the ones built and deployed in the host application. This means that the html header and the javascript links all need to match the host project. Application specific css or javascript files would need to be deployed and included then in the host application if this is required. Inline css and javascript would be deployed as part of the child views, but this should be avoided.

The demo application uses bootstrap 4 with npm and bundleconfig. The different layouts have separate background colors to demonstrate.

ASP.NET Core Navigation

The navigation between the different areas needs to be changed, because different areas or applications are used to implement the MVC apps.

The href is called using the path, for example “~/Home/Index”. This will then work for all the different applications, areas.

<nav class="bg-dark mb-4 navbar navbar-dark navbar-expand-md">
  <a href="~/Home/Index" class="navbar-brand">
	<em>HO</em>
  </a>
  <button aria-controls="navbarCollapse" 
	aria-expanded="false" 
	aria-label="Toggle navigation" 
	class="navbar-toggler" 
	data-target="#topNavbarCollapse" 
	data-toggle="collapse" type="button">
	<span class="navbar-toggler-icon"></span>
  </button>
  <div class="collapse navbar-collapse" id="topNavbarCollapse">
	<ul class="mr-auto navbar-nav">
	  <li class="nav-item">
		<a href="~/Home/Index" class="nav-link">
		@SharedLocalizer.GetLocalizedHtmlString("HOME HOST")
		</a>
	  </li>
	  <li class="nav-item">
		<a href="~/MvcApp1/Home/Index" 
		class="nav-link">MvcApp1</a>
	  </li>
	  <li class="nav-item">
		<a href="~/MvcApp2/Home/Index" 
		class="nav-link">MvcApp2</a>
	  </li>
	</ul>
  </div>
</nav>

IoC and Services

Each application has its own services, which are only required by the application itself. The different applications have also services required by all three separate projects.

The common services can be added directly to the host application. These are also added to the child applications, but only required to test or build.

The specific services can be added in a separate extension class and used in both the host application and the child application. The ServicesExtensions class implements the services for the child application. A database context could be added here, or whatever.

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MvcApp1.Models;

namespace MvcApp1
{
 public static class ServicesExtensions
  {
    public static void AddMvcApp1(
     this IServiceCollection services, IConfiguration configuration)
    {
      services.AddSingleton<ExampleService>();

      services.AddDbContext<SomeDataContext>(options =>
       options.UseSqlServer(
       configuration.GetConnectionString("SomeDataContext")
	   )
	  );
  }
 }
}

This is then used in the host application as well as the child application. The configurations for the child applications must be added to the host application configuration.

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)
{
	services.AddMvcApp1(Configuration);

	services.AddMvcApp2(Configuration);

Localization

The localization for all the projects is implemented in a shared project. This is then used in the host application as a shared localization.


  services.AddSingleton<LocService>();
  services.AddLocalization(options => 
	options.ResourcesPath = "Resources");

  services.Configure<RequestLocalizationOptions>(
	options =>
	{
		var supportedCultures = new List<CultureInfo>
			{
				new CultureInfo("en-US"),
				new CultureInfo("de-CH")
			};

		options.DefaultRequestCulture = new RequestCulture(
			culture: "en-US", 
			uiCulture: "en-US");
		options.SupportedCultures = supportedCultures;
		options.SupportedUICultures = supportedCultures;

		options.RequestCultureProviders.Insert(0, 
			new QueryStringRequestCultureProvider());
	});

  services.AddMvc()
	.AddViewLocalization()
	.AddDataAnnotationsLocalization(options =>
	{
	options.DataAnnotationLocalizerProvider = (type, factory) =>
	{
		var assemblyName = new AssemblyName(
			typeof(SharedResource).GetTypeInfo().Assembly.FullName);
		return factory.Create("SharedResource", assemblyName.Name);
	};
  }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
  ...

  var locOptions = 
   app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
  app.UseRequestLocalization(locOptions.Value);

  ...
}

To build the child applications, the LocService must also be added if this is used in one of the views, or a service.

Notes

This works with very little effort and uses ASP.NET Core more or less as it is, but also has room for lots of improvements. For example, the application specific services are registered in the host and are registered for the whole host, not just the child application. This could be improved by having application specific services. The shared css and javascript could also be optimized, maybe through an internal npm or something like that.

By hosting the different applications inside a single hosted application, the user experience can be greatly improved, the application security can be simplified, the deployment complexity is reduced and the logic remains separated.

Links

https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/areas?view=aspnetcore-2.1


Using Azure Key Vault with ASP.NET Core and Azure App Services

$
0
0

This article shows how to use an Azure Key Vault with an ASP.NET Core application deployed as an Azure App Service. The Azure App Service can use the system assigned identity to access the Key Vault. This needs to be configured in the Key Vault access policies using the service principal.

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

Create an Azure Key Vault

This is really easy, and does not require much effort. You can create an Azure Key Vault by following the Microsoft documentation here:

https://docs.microsoft.com/en-us/azure/key-vault/key-vault-get-started

Or using the Azure UI, you can create a Key Vault by clicking the “+ Create a Resource” blade and typing Key Vault in the search text input.

Fill out the inputs as required.

Now the Key Vault should be ready.

Create and Deploy the Azure App service

The second step is to deploy the ASP.NET Core application to Azure as an Azure App Service. You can do this in Visual Studio, or with templates from a build. Once the application is deployed, check that the Identity blade is configured correctly.

In the “App Services” blade, click the application which was deployed, and then the Identity blade. The Status must be “On” for the system assigned tab.

See the Microsoft docs for Azure App Services deployments.

Add the Access Policy in the Key Vault for the App Service

Now that the Azure App Service is ready, the Key Vault must be configured to permit the App Service application access. In the Key Vault, click the “Access Policies” blade, and then “Add new

Then click the “Select principal” and search for the Azure App service which was created above. Make sure that the required permissions are activated when configuring. Normally only the GET and List permissions are required. Click save, and check that the permissions are really saved after you have saved.

Now the Azure App Service can access the Key Vault.

Add some secrets to the Key Vault:

The secrets can be added in different formats. See the Microsoft docs for secret text formats. Any app.settings.json format can be matched.

Configure the application to use the Key Vault for configuration values

The application now requires code to use the Azure Key Vault. Add the Microsoft.Extensions.Configuration.AzureKeyVault NuGet package to the project.

Add the Azure Key Vault configuration to the Program.cs file in the ASP.NET Core application. Add this in the BuildWebHost method using the ConfigureAppConfiguration method. The app.settings configuration value AzureKeyVaultEndpoint should have the DNS Name value of the Key Vault. This can be found in the overview of the Key Vault which was created. Then add the Key Vault to the application as follows:

public static IWebHost BuildWebHost(string[] args) =>
 WebHost.CreateDefaultBuilder(args)
 .ConfigureAppConfiguration((context, config) =>
 {
    var builder = config.Build();

    var keyVaultEndpoint = builder["AzureKeyVaultEndpoint"];

    var azureServiceTokenProvider = new AzureServiceTokenProvider();

    var keyVaultClient = new KeyVaultClient(
      new KeyVaultClient.AuthenticationCallback(
        azureServiceTokenProvider.KeyVaultTokenCallback)
      );

    config.AddAzureKeyVault(keyVaultEndpoint);
 })
 .UseStartup<Startup>()

Remove any Configuration builders from the Startup constructor. The IConfiguration should be used, not created here.

public IConfiguration Configuration { get; }

public Startup(IConfiguration configuration, IHostingEnvironment env)
{
	JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
	Configuration = configuration;
	_environment = env;
}

Add the Key Vault developer values, to the app.settings as required, or add the secret values for development to the secrets.json file.

{
  "ConnectionStrings": {
    "RedisCacheConnection": "redis-connection-string"
  },
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  },
  "AuthConfiguration": {
    "StsServerIdentityUrl": "https://localhost:44318",
    "Audience": "mvc.hybrid.backchannel"
  },
  "SecretMvcHybridBackChannel": "secret"
}

The configuration values will be set from the Key Vault first. If no Key Vault item exists, then the secrets.json file will be used, and after this, the app.settings.json file.

The Key Vault values can be used anywhere in the ASP.NET Core application by using the standard configuration interfaces.

The following demo uses the Test configuration value which is read from the Key Vault.

private AuthConfiguration _optionsAuthConfiguration;

private IConfiguration _configuration;

public HomeController(IOptions<AuthConfiguration> optionsAuthConfiguration, IConfiguration configuration)
{
	_configuration = configuration;
	_optionsAuthConfiguration = optionsAuthConfiguration.Value;
}

public IActionResult Index()
{
	var cs = _configuration["Test"];
	return View("Index",  cs);
}

Links

https://social.technet.microsoft.com/wiki/contents/articles/51871.net-core-2-managing-secrets-in-web-apps.aspx#AzureKeyVault_Secrets

https://docs.microsoft.com/en-us/azure/key-vault/key-vault-developers-guide

https://jeremylindsayni.wordpress.com/2018/03/15/using-the-azure-key-vault-to-keep-secrets-out-of-your-web-apps-source-code/

https://stackoverflow.com/questions/40025598/azure-key-vault-access-denied

https://cmatskas.com/securing-asp-net-core-application-settings-using-azure-key-vault/

https://github.com/jayendranarumugam/DemoSecrets/tree/master/DemoSecrets

https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-windows?view=azure-cli-latest

Passing Javascript values to ASP.NET Core View components

$
0
0

In this post, I show how an ASP.NET Core MVC view can send a Javascript parameter value to an ASP.NET Core view component. Invoking a view component in the view using ‘@await Component.InvokeAsync’ will not work, as this is rendered before the Javascript value has been created.

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

History

2019-01-24 Added an Anti-Forgery token to Javascript view component ajax request

Creating the view component

The view component is setup in the standard way as described in the Microsoft docs:

The view component called MyComponent was created which uses the MyComponentModel model. This model is used to pass the parameters to the component and also to display the view. The ScreenWidth property is read from a Javascript value, and set in the model.

using AspNetCoreBootstrap4Validation.ViewModels;
using Microsoft.AspNetCore.Mvc;

namespace AspNetCoreBootstrap4Validation.Views
{
    public class MyComponent : ViewComponent
    {
        public MyComponent() {}

        public IViewComponentResult Invoke(MyComponentModel model)
        {
            model.ScreenWidth = "Read on the server:" + model.ScreenWidth;
            return View(model);
        }
    }
}

The Default.cshtml view for the component displays the values as required.

@using AspNetCoreBootstrap4Validation.ViewModels
@model MyComponentModel

@{
    ViewData["Title"] = "View Component";
}

<h5>Result from Component:server</h5>

<text><em>ScreenWidth: </em>@Model.ScreenWidth</text>

<br />
<text><em>Name:</em> @Model.StandardValidation.Name</text><br />
<text><em>IsCool:</em> @Model.StandardValidation.IsCool</text><br />
<text><em>Age: </em>@Model.StandardValidation.Age</text><br />

An action method in an ASP.NET Core MVC controller is used to call the view component.

public class AjaxWithComponentController : Controller
{
        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult LoadComponent(MyComponentModel model)
        {
            return ViewComponent("MyComponent", model);
        }

Javascript using jQuery and Ajax is used to request the action method in the MVC controller, which calls the view component. The screenWidth uses the document.documentElement.clientWidth value to get the screen width and the form is serialized and sent as a Json object in the model used to request the view component.

The Anti-Forgery token needs to be added to the header for each request. See the Microsoft Docs for details.

 <script language="javascript">

        function loadComponentView() {

            var paramsFromForm = {};
            $.each($("#partialformAjaxWithComponent").serializeArray(), function (index, value) {
                paramsFromForm[value.name] = paramsFromForm[value.name] ? paramsFromForm[value.name] || value.value : value.value;
            });

            var componentData = {};

            componentData.standardValidation = paramsFromForm;
            componentData.screenWidth = document.documentElement.clientWidth;

            console.log(componentData);

            $.ajax({
                url: window.location.origin + "/AjaxWithComponent/LoadComponent",
                type: "post",
                dataType: "json",
                beforeSend: function (x) {
                    if (x && x.overrideMimeType) {
                        x.overrideMimeType("application/json;charset=UTF-8");
                    };
                    x.setRequestHeader('RequestVerificationToken', document.getElementById('RequestVerificationToken').value);   
                },
                data: componentData,
                complete: function (result) {
                    console.log(result.responseText);
                    $("#partialComponentResult").html(result.responseText);
                }
            });
        };

    </script>

The result of the ajax request is displayed in the div with the id partialComponentResult.

@model AspNetCoreBootstrap4Validation.ViewModels.StandardValidationModel
@{
    ViewData["Title"] = "Ajax with Component Page";
}

@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf
@functions{
    public string GetAntiXsrfRequestToken()
    {
        return Xsrf.GetAndStoreTokens(Context).RequestToken;
    }
}

<input type="hidden" id="RequestVerificationToken"
       name="RequestVerificationToken" value="@GetAntiXsrfRequestToken()">

<h4>Ajax with Component View</h4>

<div id="partialComponentResult">
    <h5>change a value to do a component load</h5>

    <text><em>ScreenWidth:</em> ...</text>

    <br />
    <text><em>Name:</em> ...</text><br />
    <text><em>IsCool:</em> ...</text><br />
    <text><em>Age:</em> ...</text><br />
</div>

<hr />

<form id="partialformAjaxWithComponent" onchange="loadComponentView()" method="post" asp-action="Index" asp-controller="AjaxWithComponent">

    <div asp-validation-summary="All" class="text-danger"></div>

    <div class="form-group">
        <label for="name">Name</label>
        <input type="text" class="form-control" asp-for="Name" id="name" aria-describedby="nameHelp" placeholder="Enter name">
        <small id="nameHelp" class="form-text text-muted">We'll never share your name ...</small>
        <span asp-validation-for="Name" class="text-danger"></span>
    </div>
    <div class="form-group">
        <label for="age">Age</label>
        <input type="number" class="form-control" id="age" asp-for="Age" placeholder="0">
        <span asp-validation-for="Age" class="text-danger"></span>
    </div>
    <div class="form-check ten_px_bottom">
        <input type="checkbox" class="form-check-input big_checkbox" asp-for="IsCool" id="IsCool">
        <label class="form-check-label ten_px_left" for="IsCool">IsCool</label>
        <span asp-validation-for="IsCool" class="text-danger"></span>
    </div>


    <button type="submit" class="btn btn-primary">Submit</button>
</form>

Links:

https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components?view=aspnetcore-2.2

https://andrewlock.net/passing-variables-to-a-view-component/

https://mariusschulz.com/blog/view-components-in-asp-net-mvc-6

ASP.NET Core 2.0 MVC View Components

https://docs.microsoft.com/en-us/aspnet/core/security/anti-request-forgery?view=aspnetcore-2.2#javascript-ajax-and-spas

Auto Generated .NET API Clients using NSwag and Swashbuckle Swagger

$
0
0

This article shows how auto generated code for a C# HTTP API client could be created using Swagger and NSwag . The API was created using ASP.NET Core MVC.

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

Create the API using ASP.NET Core and Swashbuckle Swagger

The API is created using ASP.NET Core with Swashbuckle. Add the required Nuget packages to the project, set the GenerateDocumentationFile element to true and also add the NoWarn element, if all the C# code is not documented.

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>netcoreapp2.2</TargetFramework>
  </PropertyGroup>

  <PropertyGroup>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
    <NoWarn>$(NoWarn);1591</NoWarn>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="4.0.1" />
    <PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="4.0.1" />
    <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="4.0.1" />
    <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="4.0.1" />
    <PackageReference Include="WebApiContrib.Core.Formatter.Csv" Version="3.0.0" />
  </ItemGroup>

</Project>

In the Startup class, add the Swagger configuration in the ConfigureServices method. The AddSwaggerGen extension method uses the XML file for the comments.

public void ConfigureServices(IServiceCollection services)
{
	services.AddSwaggerGen(c =>
	{
		c.SwaggerDoc("v1", new Info
		{
			Version = "v1",
			Title = "CSV TEST API",
		});

		// comments path
		var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
		var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
		c.IncludeXmlComments(xmlPath);
	});

Use the Swagger middleware to create the UI and the Json file with the API documentation. The UI part is not required for NSwag.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
	app.UseStaticFiles();
	app.UseMvc();

	app.UseSwagger();
	app.UseSwaggerUI(c =>
	{
		c.SwaggerEndpoint("/swagger/v1/swagger.json", "CSV Test API V1");
	});
}

Add an API as required. Here is a basic example of a CRUD REST API with definitions, which will be picked up by the Swagger documentation.

using System.Collections.Generic;
using System.Linq;
using System.Net;
using CsvWebApiSwagger.Models;
using Microsoft.AspNetCore.Mvc;

namespace CsvWebApiSwagger.Controllers
{
    /// <summary>
    /// Jobs API for CRUD job 
    /// </summary>
    [Route("api/[controller]")]
    public class JobsController : Controller
    {
        /// <summary>
        /// Gets all jobs using the API
        /// </summary>
        /// <returns>Return a list of jobs</returns>
        [HttpGet]
        [ProducesResponseType(typeof(IEnumerable<Job>), (int)HttpStatusCode.OK)]
        public IActionResult Get()
        {
            return Ok(Jobs);
        }

        /// <summary>
        /// get Job using the id
        /// </summary>
        /// <param name="id">job id</param>
        /// <returns>Job for the ID</returns>
        [HttpGet("{id}")]
        [ProducesResponseType(typeof(IEnumerable<Job>), (int)HttpStatusCode.OK)]
        public IActionResult Get(int id)
        {
            if (id == 0)
            {
                return BadRequest();
            }

            var jobExists = Jobs.Exists(j => j.Id == id);

            if (jobExists == false)
            {
                return NotFound($"Job with Id not found: {id}");
            }

            return Ok(Jobs.First(j => j.Id == id));
        }

        /// <summary>
        /// Creates a new JOB if the ID does not already exist
        /// </summary>
        /// <remarks>
        /// Sample create Job:
        ///
        ///     POST api/Jobs
        ///     {
        ///        "id": int id which does not exist,
        ///        "title": "title of the job",
        ///        "description": "Description of the job",
        ///        "level": "level of the job",
        ///        "requirements": "Requirements of the job",
        ///     }
        ///
        /// </remarks>
        /// <param name="job">Job to create</param>
        /// <returns>The created JOB</returns>
        [HttpPost]
        [ProducesResponseType(typeof(Job), (int)HttpStatusCode.OK)]
        [ProducesResponseType(typeof(Job), (int)HttpStatusCode.Conflict)]
        public IActionResult Post([FromBody]Job job)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest();
            }

            var jobAlreadyExists = Jobs.Exists(j => j.Id == job.Id);

            if (jobAlreadyExists == true)
            {
                return Conflict($"Job with Id {job.Id} exists");
            }

            Jobs.Add(job);

            return Ok(job);
        }

        /// <summary>
        /// put a string
        /// </summary>
        /// <param name="id">id of a string</param>
        /// <param name="value">value of the string</param>
        [HttpPut("{id}")]
        [ProducesResponseType(typeof(Job), (int)HttpStatusCode.OK)]
        public IActionResult Put(int id, [FromBody]Job value)
        {
            if (id == 0)
            {
                return BadRequest();
            }

            if (!ModelState.IsValid)
            {
                return BadRequest();
            }

            var job = Jobs.First(j => j.Id == id);
            job.Description = value.Description;
            job.Level = value.Level;
            job.Title = value.Title;
            job.Requirements = value.Requirements;

            return Ok(value);
        }

        /// <summary>
        /// delete the Job if it is found
        /// </summary>
        /// <param name="id">job id</param>
        [HttpDelete("{id}")]
        [ProducesResponseType((int)HttpStatusCode.OK)]
        [ProducesResponseType((int)HttpStatusCode.NotFound)]
        [ProducesResponseType((int)HttpStatusCode.Conflict)]
        public IActionResult Delete(int id)
        {
            if (id == 0)
            {
                return BadRequest();
            }

            var job = Jobs.First(j => j.Id == id);

            if (job == null)
            {
                return Conflict($"Job with Id {id} does not exist");
            }

            Jobs.Remove(job);
            return Ok();
        }
    }
}

Creating the API client using NSwag

NSwag can be used to create a C# class, which implements the client for the API. This can be created using the NSwagStudio created by Rico Suter.

Download this, install it and open it. Then configure the tool, to read from the API. (Start the API first). Set the namespace to the same as the target project, and save to class where it is required.

This generated class can then be used in any application, and for a Console .NET Core application, only the Json Nuget package is required.

Here is a simple example of the API usage.

using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace ConsoleApiClient
{
    class Program
    {
        public static void Main(string[] args) => MainAsync().GetAwaiter().GetResult();

        static async Task MainAsync()
        {
            Console.WriteLine("begin...");

            HttpClient httpClient = new HttpClient();

            var clientCsvWebApiSwagger = new ClientCsvWebApiSwagger(
                "https://localhost:44354/", httpClient);

            var all = await clientCsvWebApiSwagger.GetAllAsync();

            Console.WriteLine($"amount of jobs: {all.Count}");

            Console.WriteLine($"Create job: Id = 2340");

            var job = await clientCsvWebApiSwagger.PostAsync(new Job
            {
                Id = 2340,
                Description = "created using the NSwag generated code",
                Level = "amazing",
                Requirements = "Json nugetg package",
                Title = "NSwag generate"
            });

            all = await clientCsvWebApiSwagger.GetAllAsync();

            Console.WriteLine($"amount of jobs: {all.Count}");
            Console.ReadLine();

        }
    }
}

The NSwag configuration can be saved and commited to the project for reuse later.

{
  "runtime": "NetCore22",
  "defaultVariables": null,
  "swaggerGenerator": {
    "fromSwagger": {
      "json": "{\r\n  \"swagger\": \"2.0\",\r\n  \"info\": {\r\...}",
      "url": "https://localhost:44354/swagger/v1/swagger.json",
      "output": null
    }
  },
  "codeGenerators": {
    "swaggerToCSharpClient": {
      "clientBaseClass": null,
      "configurationClass": null,
      "generateClientClasses": true,
      "generateClientInterfaces": false,
      "generateDtoTypes": true,
      "injectHttpClient": true,
      "disposeHttpClient": true,
      "protectedMethods": [],
      "generateExceptionClasses": true,
      "exceptionClass": "SwaggerException",
      "wrapDtoExceptions": true,
      "useHttpClientCreationMethod": false,
      "httpClientType": "System.Net.Http.HttpClient",
      "useHttpRequestMessageCreationMethod": false,
      "useBaseUrl": true,
      "generateBaseUrlProperty": true,
      "generateSyncMethods": false,
      "exposeJsonSerializerSettings": false,
      "clientClassAccessModifier": "public",
      "typeAccessModifier": "public",
      "generateContractsOutput": false,
      "contractsNamespace": null,
      "contractsOutputFilePath": null,
      "parameterDateTimeFormat": "s",
      "generateUpdateJsonSerializerSettingsMethod": true,
      "serializeTypeInformation": false,
      "queryNullValue": "",
      "className": "{controller}ClientCsvWebApiSwagger",
      "operationGenerationMode": "MultipleClientsFromOperationId",
      "additionalNamespaceUsages": [],
      "additionalContractNamespaceUsages": [],
      "generateOptionalParameters": false,
      "generateJsonMethods": true,
      "enforceFlagEnums": false,
      "parameterArrayType": "System.Collections.Generic.IEnumerable",
      "parameterDictionaryType": "System.Collections.Generic.IDictionary",
      "responseArrayType": "System.Collections.Generic.ICollection",
      "responseDictionaryType": "System.Collections.Generic.IDictionary",
      "wrapResponses": false,
      "wrapResponseMethods": [],
      "generateResponseClasses": true,
      "responseClass": "SwaggerResponse",
      "namespace": "ConsoleApiClient",
      "requiredPropertiesMustBeDefined": true,
      "dateType": "System.DateTimeOffset",
      "jsonConverters": null,
      "dateTimeType": "System.DateTimeOffset",
      "timeType": "System.TimeSpan",
      "timeSpanType": "System.TimeSpan",
      "arrayType": "System.Collections.Generic.ICollection",
      "arrayInstanceType": "System.Collections.ObjectModel.Collection",
      "dictionaryType": "System.Collections.Generic.IDictionary",
      "dictionaryInstanceType": "System.Collections.Generic.Dictionary",
      "arrayBaseType": "System.Collections.ObjectModel.Collection",
      "dictionaryBaseType": "System.Collections.Generic.Dictionary",
      "classStyle": "Poco",
      "generateDefaultValues": true,
      "generateDataAnnotations": true,
      "excludedTypeNames": [],
      "handleReferences": false,
      "generateImmutableArrayProperties": false,
      "generateImmutableDictionaryProperties": false,
      "jsonSerializerSettingsTransformationMethod": null,
      "inlineNamedDictionaries": false,
      "inlineNamedTuples": true,
      "templateDirectory": null,
      "typeNameGeneratorType": null,
      "propertyNameGeneratorType": null,
      "enumNameGeneratorType": null,
      "serviceHost": null,
      "serviceSchemes": null,
      "output": "ConsoleApiClient/MyApiClient.cs"
    }
  }
}

When the applications are started, the API can be used and no client code, models need to be implemented manually.

Links

https://docs.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-nswag

https://github.com/RSuter/NSwag

https://docs.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-swashbuckle

https://github.com/RSuter/NSwag/wiki/NSwagStudio

https://swagger.io/

https://github.com/dmitry-pavlov/openapi-connected-service

Security Experiments with gRPC and ASP.NET Core 3.0

$
0
0

This article shows how a gRPC service could implement OAuth2 security using IdentityServer4 as the token service.

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

Posts in this series

History

2019-04-20: Updated Nuget packages, .NET Core 3 preview4 changes
2019-03-08: Removing the IHttpContextAccessor, no longer required
2019-03-07: Updated the auth security to configure this on the route, attributes are not supported in the current preview.

Setup

The application is implemented using 3 applications. A console application is used as the gRPC client. This application requests an access token for the gRPC server using the IdentityServer4 token service. The client application then sends the access token in the header of the HTTP2 request. The gRPC server then validates the token using Introspection, and if the token is valid, the data is returned. If the token is not valid, a RPC exception is created and sent back to the server.

At present, as this code is still in production, securing the API using the Authorization attributes with policies does not seem to work, so as a quick fix, the policy is added to the routing configuration.

The gRPC client and server were setup using the Visual Studio template for gRPC.

gRPC Server

The GreeterService class is the generated class from the Visual Studio template. The security bits were then added to this class. The Authorize attribute is added to the class which is how the security should work.

using System.Threading.Tasks;
using Greet;
using Grpc.Core;
using Microsoft.AspNetCore.Authorization;

namespace Secure_gRpc
{
    [Authorize(Policy = "protectedScope")]
    public class GreeterService : Greeter.GreeterBase
    {
        public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
        {
            return Task.FromResult(new HelloReply
            {
                Message = "Hello " + request.Name
            });
        }
    }
}

The startup class configures the gRPC service and the required security to use this service. IdentityServer4.AccessTokenValidation is used to validate the access token using introspection. The gRPC service is added along with the authorization and the authentication.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using IdentityServer4.AccessTokenValidation;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using System.Security.Claims;

namespace Secure_gRpc
{
    public class Startup
    {
        private string stsServer = "https://localhost:44352";

        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddHttpContextAccessor();

            services.AddAuthorization(options =>
            {
                options.AddPolicy("protectedScope", policy =>
                {
                    policy.RequireClaim("scope", "grpc_protected_scope");
                });
            });

            services.AddAuthorizationPolicyEvaluator();

            services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
                .AddIdentityServerAuthentication(options =>
                {
                    options.Authority = stsServer;
                    options.ApiName = "ProtectedGrpc";
                    options.ApiSecret = "grpc_protected_secret";
                    options.RequireHttpsMetadata = false;
                });

            services.AddGrpc(options =>
            {
                options.EnableDetailedErrors = true;
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseStaticFiles();
            app.UseHttpsRedirection();

            app.UseRouting();

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

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGrpcService<GreeterService>().RequireAuthorization("protectedScope");
                endpoints.MapGrpcService<DuplexService>().RequireAuthorization("protectedScope");
                endpoints.MapRazorPages();
            });
        }
    }
}

The gRPC service is then setup to run using HTTPS and HTTP2.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.Hosting;

namespace Secure_gRpc
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>()
                    .ConfigureKestrel(options =>
                    {
                        options.Limits.MinRequestBodyDataRate = null;
                        options.ListenLocalhost(50051, listenOptions =>
                        {
                            listenOptions.UseHttps("server.pfx", "1111");
                            listenOptions.Protocols = HttpProtocols.Http2;
                        });
                    });
                });
    }
}

RPC interface definition

The RPC API is defined using proto3 and referenced in both projects. When the applications are built, the C# classes are created.

syntax = "proto3";

package Greet;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings.
message HelloReply {
  string message = 1;
}

gRPC client

The client is implemented in a simple console application. This client gets the access token from the IdentityServer4 token service, and adds it to the Authorization header as a bearer token. The client then uses a cert to connect over HTTPS. This code will probably change before the release. Then the API is called and the data is returned, or an exception. If you comment in the incorrect token, an auth exception is returned.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading.Tasks;
using Greet;
using Grpc.Core;

namespace Secure_gRpc
{
    public class Program
    {
        static async Task Main(string[] args)
        {
            ///
            /// Token init
            /// 
            HttpClient httpClient = new HttpClient();
            ApiService apiService = new ApiService(httpClient);

            // switch the token here to use an invalid token,
            var token = await apiService.GetAccessTokenAsync();
            //var token = "This is invalid, I hope it fails";

            var tokenValue = "Bearer " + token;
            var metadata = new Metadata
            {
                { "Authorization", tokenValue }
            };

            ///
            /// Call gRPC HTTPS
            ///
            var channelCredentials =  new SslCredentials(
                File.ReadAllText("Certs\\ca.crt"),
                    new KeyCertificatePair(
                        File.ReadAllText("Certs\\client.crt"),
                        File.ReadAllText("Certs\\client.key")
                    )
                );

            CallOptions callOptions = new CallOptions(metadata);
            // Include port of the gRPC server as an application argument
            var port = args.Length > 0 ? args[0] : "50051";
            var channel = new Channel("localhost:" + port, channelCredentials);
            var client = new Greeter.GreeterClient(channel);

            var reply = await client.SayHelloAsync(
                new HelloRequest { Name = "GreeterClient" }, callOptions);

            Console.WriteLine("Greeting: " + reply.Message);

            await channel.ShutdownAsync();

            Console.WriteLine("Press any key to exit...");
            Console.ReadKey();
        }
   }
}

Sending a valid token

Sending an invalid token

This code is still in development, and a lot will change before the first release. The demo shows some of the new gRPC, HTTP2, hosting features which will be released as part of ASP.NET Core 3.0.

Links:

https://github.com/grpc/grpc-dotnet/

https://grpc.io/

An Early Look at gRPC and ASP.NET Core 3.0

https://www.zoeys.blog/first-impressions-of-grpc-integration-in-asp-net-core-3-preview/

Running Razor Pages and a gRPC service in a single ASP.NET Core application

$
0
0

This article shows how ASP.NET Core Razor Pages can be run in the same application as a gRPC service.

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

Posts in this series

Adding Razor Pages to an existing gRPC service

This demo is built using the code created in the previous post, which is pretty much the gRPC code created from the ASP.NET Core 3.0 templates.

To support both Razor Pages and gRPC services hosted using a single Kestrel server, both HTTP and HTTP2 needs to be supported. This is done in the Program class of the application using a ConfigureKestrel method. Set the Protocols property to HttpProtocols.Http1AndHttp2.

public static IHostBuilder CreateHostBuilder(string[] args) =>
 Host.CreateDefaultBuilder(args)
   .ConfigureWebHostDefaults(webBuilder =>
   {
	webBuilder.UseStartup<Startup>()
	.ConfigureKestrel(options =>
	{
		options.Limits.MinRequestBodyDataRate = null;
		options.ListenLocalhost(50051, listenOptions =>
		{
			listenOptions.UseHttps("server.pfx", "1111");
			listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
		});
	});
   });

Now we need to add the MVC middleware and also the Newtonsoft JSON Nuget package ( Microsoft.AspNetCore.Mvc.NewtonsoftJson ) as this is no longer in the default Nuget packages in ASP.NET Core 3.0. You need to add the Nuget package to the project.

Then add the MVC middleware.

public void ConfigureServices(IServiceCollection services)
{
	...

	services.AddGrpc(options =>
	{
		options.EnableDetailedErrors = true;
	});

	services.AddMvc()
	   .AddNewtonsoftJson();
}

In the configure method, the static files middleware need to be added so that the css and the Javascript will be supported. The Razor Pages are then added to the routing.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	...

	app.UseStaticFiles();

	app.UseRouting(routes =>
	{
		routes.MapGrpcService<GreeterService>();
		   
		routes.MapRazorPages();
	});

}

Add the Razor Pages to the application and also the css, Javascript files inside the wwwroot.

And now both application types are running, hosted on Kestrel in the same APP which is really cool.

Links:

https://github.com/grpc/grpc-dotnet/

https://grpc.io/

An Early Look at gRPC and ASP.NET Core 3.0

Using Azure Service Bus Queues with ASP.NET Core Services

$
0
0

This article shows how to implement two ASP.NET Core API applications to communicate with each other using Azure Service Bus. The ASP.NET Core APIs are implemented with Swagger support and uses an Azure Service Bus Queue to send data from one service to the other ASP.NET Core application.

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

Posts in this series:

Setting up the Azure Service Bus Queue

Azure Service Bus is setup as described here:

https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-create-namespace-portal

A queue or a topic can be used to implement the messaging. A queue is used as the messaging type in this example. Once the data has been received, it is removed from the queue.

Applications Overview

The applications are implemented as follows:

Implementing a Service Bus Queue

The Microsoft.Azure.ServiceBus Nuget package is used to implement the Azure Service Bus clients. The connection string for the service bus is saved in the user secrets of the projects. To run the example yourself, create your own Azure Service Bus, and set the secret for the projects. This can be easily done in Visual Studio by right clicking the project menu in the solution explorer. When deploying the application, use Azure Key Vault to set the secret. This would need to be implemented in the applications.

The SendMessage method takes a MyPayload type as a parameter, and adds this to the message as a Json payload.

using Microsoft.Azure.ServiceBus;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using System.Text;
using System.Threading.Tasks;

namespace ServiceBusMessaging
{
    public class ServiceBusSender
    {
        private readonly QueueClient _queueClient;
        private readonly IConfiguration _configuration;
        private const string QUEUE_NAME = "simplequeue";

        public ServiceBusSender(IConfiguration configuration)
        {
            _configuration = configuration;
            _queueClient = new QueueClient(
              _configuration.GetConnectionString("ServiceBusConnectionString"), 
              QUEUE_NAME);
        }
        
        public async Task SendMessage(MyPayload payload)
        {
            string data = JsonConvert.SerializeObject(payload);
            Message message = new Message(Encoding.UTF8.GetBytes(data));

            await _queueClient.SendAsync(message);
        }
    }
}

The ServiceBusSender is registered to the IoC of the ASP.NET Core application in the Startup class, ConfigureServices method. Swagger is also added here.

public void ConfigureServices(IServiceCollection services)
{
	services.AddMvc()
		.AddNewtonsoftJson();

	services.AddScoped<ServiceBusSender>();

	services.AddSwaggerGen(c =>
	{
		c.SwaggerDoc("v1", new OpenApiInfo
		{
			Version = "v1",
			Title = "Payload View API",
		});
	});
}

This service can then be used in the Controller which provides the API.

[HttpPost]
[ProducesResponseType(typeof(Payload), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Payload), StatusCodes.Status409Conflict)]
public async Task<IActionResult> Create([FromBody][Required] Payload request)
{
	if (data.Any(d => d.Id == request.Id))
	{
		return Conflict($"data with id {request.Id} already exists");
	}

	data.Add(request);

	// Send this to the bus for the other services
	await _serviceBusSender.SendMessage(new MyPayload
	{
		Goals = request.Goals,
		Name = request.Name,
		Delete = false
	});

	return Ok(request);
}

Consuming messaging from the Queue

The ServiceBusConsumer implements the IServiceBusConsumer interface. This is used to receive the messages from Azure Service Bus. The Connection String from the Queue is read from the application IConfiguration interface. The RegisterOnMessageHandlerAndReceiveMessages method adds the event handler for the messages, and uses the ProcessMessagesAsync method to process these. The ProcessMessagesAsync method converts the message to an object and calls the IProcessData interface to complete the processing of the message.

using Microsoft.Azure.ServiceBus;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ServiceBusMessaging
{
    public interface IServiceBusConsumer
    {
        void RegisterOnMessageHandlerAndReceiveMessages();
        Task CloseQueueAsync();
    }

    public class ServiceBusConsumer : IServiceBusConsumer
    {
        private readonly IProcessData _processData;
        private readonly IConfiguration _configuration;
        private readonly QueueClient _queueClient;
        private const string QUEUE_NAME = "simplequeue";
        private readonly ILogger _logger;

        public ServiceBusConsumer(IProcessData processData, 
            IConfiguration configuration, 
            ILogger<ServiceBusConsumer> logger)
        {
            _processData = processData;
            _configuration = configuration;
            _logger = logger;
            _queueClient = new QueueClient(
              _configuration.GetConnectionString("ServiceBusConnectionString"), QUEUE_NAME);
        }

        public void RegisterOnMessageHandlerAndReceiveMessages()
        {
            var messageHandlerOptions = new MessageHandlerOptions(ExceptionReceivedHandler)
            {
                MaxConcurrentCalls = 1,
                AutoComplete = false
            };

            _queueClient.RegisterMessageHandler(ProcessMessagesAsync, messageHandlerOptions);
        }

        private async Task ProcessMessagesAsync(Message message, CancellationToken token)
        {
            var myPayload = JsonConvert.DeserializeObject<MyPayload>(Encoding.UTF8.GetString(message.Body));
            _processData.Process(myPayload);
            await _queueClient.CompleteAsync(message.SystemProperties.LockToken);
        }

        private Task ExceptionReceivedHandler(ExceptionReceivedEventArgs exceptionReceivedEventArgs)
        {
            _logger.LogError(exceptionReceivedEventArgs.Exception, "Message handler encountered an exception");
            var context = exceptionReceivedEventArgs.ExceptionReceivedContext;

            _logger.LogDebug($"- Endpoint: {context.Endpoint}");
            _logger.LogDebug($"- Entity Path: {context.EntityPath}");
            _logger.LogDebug($"- Executing Action: {context.Action}");

            return Task.CompletedTask;
        }

        public async Task CloseQueueAsync()
        {
            await _queueClient.CloseAsync();
        }
    }
}

The Startup class configures the application and adds the support for Azure Service Bus, Swagger and the ASP.NET Core application.

public void ConfigureServices(IServiceCollection services)
{
	services.AddMvc()
		.AddNewtonsoftJson();

	services.AddSingleton<IServiceBusConsumer, ServiceBusConsumer>();
	services.AddTransient<IProcessData, ProcessData>();

	services.AddSwaggerGen(c =>
	{
		c.SwaggerDoc("v1", new OpenApiInfo
		{
			Version = "v1",
			Title = "Payload API",
		});
	});
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{

	app.UseStaticFiles();
	app.UseHttpsRedirection();

	app.UseRouting();

	app.UseAuthorization();
	app.UseCors();

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

	// Enable middleware to serve generated Swagger as a JSON endpoint.
	app.UseSwagger();
	app.UseSwaggerUI(c =>
	{
	    c.SwaggerEndpoint("/swagger/v1/swagger.json", 
	       "Payload Management API V1");
	});

	var bus = app.ApplicationServices.GetService<IServiceBusConsumer>();
	bus.RegisterOnMessageHandlerAndReceiveMessages();
}

The IProcessData interface is added to the shared library. This is used to process the incoming messages in the ServiceBusConsumer service.

public interface IProcessData
{
	void Process(MyPayload myPayload);
}

The ProcessData implements the IProcessData and is added to the application which receives the messages. The hosting application can the do whatever is required with the messages.

using AspNetCoreServiceBusApi2.Model;
using ServiceBusMessaging;

namespace AspNetCoreServiceBusApi2
{
    public class ProcessData : IProcessData
    {
        public void Process(MyPayload myPayload)
        {
            DataServiceSimi.Data.Add(new Payload
            {
                Name = myPayload.Name,
                Goals = myPayload.Goals
            });
        }
    }
}

When the applications are started, a POST request can be sent using the swagger UI from the first App.

And the message is then processed in the API 2.

Links:

https://docs.microsoft.com/en-us/azure/service-bus-messaging/

https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-dotnet-get-started-with-queues

https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/multi-container-microservice-net-applications/integration-event-based-microservice-communications

https://www.nuget.org/packages/Microsoft.Azure.ServiceBus

Always subscribe to Dead-lettered messages when using an Azure Service Bus

Using Azure Service Bus Topics in ASP.NET Core

$
0
0

This article shows how to implement two ASP.NET Core API applications to communicate with each other using Azure Service Bus Topics. This post continues on from the last article, this time using topics and subscriptions to communicate instead of a queue. By using a topic with subscriptions, and message can be sent to n receivers.

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

Posts in this series:

Setting up the Azure Service Bus Topics

The Azure Service Bus Topic and the Topic Subscription need to be setup in Azure, either using the portal or scripts.

You need to create a Topic in the Azure Service Bus:

In the new Topic, add a Topic Subscription:

ASP.NET Core applications

The applications are setup like in the first post in this series. This time the message bus uses a topic and a subscription to send the messages.

Implementing the Azure Service Bus Topic sender

The messages are sent using the ServiceBusTopicSender class. This class uses the Azure Service Bus connection string and a topic path which matches what was configured in the Azure portal. A new TopicClient is created, and this can then be used to send messages to the topic.

using Microsoft.Azure.ServiceBus;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Text;
using System.Threading.Tasks;

namespace ServiceBusMessaging
{
    public class ServiceBusTopicSender
    {
        private readonly TopicClient _topicClient;
        private readonly IConfiguration _configuration;
        private const string TOPIC_PATH = "mytopic";
        private readonly ILogger _logger;

        public ServiceBusTopicSender(IConfiguration configuration, 
            ILogger<ServiceBusTopicSender> logger)
        {
            _configuration = configuration;
            _logger = logger;
            _topicClient = new TopicClient(
                _configuration.GetConnectionString("ServiceBusConnectionString"),
                TOPIC_PATH
            );
        }
        
        public async Task SendMessage(MyPayload payload)
        {
            string data = JsonConvert.SerializeObject(payload);
            Message message = new Message(Encoding.UTF8.GetBytes(data));

            try
            {
                await _topicClient.SendAsync(message);
            }
            catch (Exception e)
            {
                _logger.LogError(e.Message);
            }
        }
    }
}

The ServiceBusTopicSender class is added as a service in the Startup class.

services.AddScoped<ServiceBusTopicSender>();

This service can then be used in the API to send messages to the bus, when other services need the data from the API call.

[HttpPost]
[ProducesResponseType(typeof(Payload), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Payload), StatusCodes.Status409Conflict)]
public async Task<IActionResult> Create([FromBody][Required] Payload request)
{
	if (data.Any(d => d.Id == request.Id))
	{
		return Conflict($"data with id {request.Id} already exists");
	}

	data.Add(request);

	// Send this to the bus for the other services
	await _serviceBusTopicSender.SendMessage(new MyPayload
	{
		Goals = request.Goals,
		Name = request.Name,
		Delete = false
	});

	return Ok(request);
}

Implementing an Azure Service Bus Topic Subscription

The ServiceBusTopicSubscription class implements the topic subscription. The SubscriptionClient is created using the Azure Service Bus connection string, the topic path and the subscription name. These values are the values which have been configured in Azure. The RegisterOnMessageHandlerAndReceiveMessages method is used to receive the events and send the messages on for processing in the IProcessData implementation.

using Microsoft.Azure.ServiceBus;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ServiceBusMessaging
{
    public interface IServiceBusTopicSubscription
    {
        void RegisterOnMessageHandlerAndReceiveMessages();
        Task CloseSubscriptionClientAsync();
    }

    public class ServiceBusTopicSubscription : IServiceBusTopicSubscription
    {
        private readonly IProcessData _processData;
        private readonly IConfiguration _configuration;
        private readonly SubscriptionClient _subscriptionClient;
        private const string TOPIC_PATH = "mytopic";
        private const string SUBSCRIPTION_NAME = "mytopicsubscription";
        private readonly ILogger _logger;

        public ServiceBusTopicSubscription(IProcessData processData, 
            IConfiguration configuration, 
            ILogger<ServiceBusTopicSubscription> logger)
        {
            _processData = processData;
            _configuration = configuration;
            _logger = logger;

            _subscriptionClient = new SubscriptionClient(
                _configuration.GetConnectionString("ServiceBusConnectionString"), 
                TOPIC_PATH, 
                SUBSCRIPTION_NAME);
        }

        public void RegisterOnMessageHandlerAndReceiveMessages()
        {
            var messageHandlerOptions = new MessageHandlerOptions(ExceptionReceivedHandler)
            {
                MaxConcurrentCalls = 1,
                AutoComplete = false
            };

            _subscriptionClient.RegisterMessageHandler(ProcessMessagesAsync, messageHandlerOptions);
        }

        private async Task ProcessMessagesAsync(Message message, CancellationToken token)
        {
            var myPayload = JsonConvert.DeserializeObject<MyPayload>(Encoding.UTF8.GetString(message.Body));
            _processData.Process(myPayload);
            await _subscriptionClient.CompleteAsync(message.SystemProperties.LockToken);
        }

        private Task ExceptionReceivedHandler(ExceptionReceivedEventArgs exceptionReceivedEventArgs)
        {
            _logger.LogError(exceptionReceivedEventArgs.Exception, "Message handler encountered an exception");
            var context = exceptionReceivedEventArgs.ExceptionReceivedContext;

            _logger.LogDebug($"- Endpoint: {context.Endpoint}");
            _logger.LogDebug($"- Entity Path: {context.EntityPath}");
            _logger.LogDebug($"- Executing Action: {context.Action}");

            return Task.CompletedTask;
        }

        public async Task CloseSubscriptionClientAsync()
        {
            await _subscriptionClient.CloseAsync();
        }
    }
}

The IServiceBusTopicSubscription and the IProcessData, plus the implementations are added to the IoC of the ASP.NET Core application.

services.AddSingleton<IServiceBusTopicSubscription, ServiceBusTopicSubscription>();
services.AddTransient<IProcessData, ProcessData>();

The RegisterOnMessageHandlerAndReceiveMessages is called in the Configure Startup method, so that the application starts to listen for messages.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	...

	var busSubscription = 
		app.ApplicationServices.GetService<IServiceBusTopicSubscription>();
	busSubscription.RegisterOnMessageHandlerAndReceiveMessages();
}

The ProcessData service processes the incoming topic messages for the defined subscription, and adds them to an in-memory list in this demo, which can be viewed using the Swagger API.

using AspNetCoreServiceBusApi2.Model;
using ServiceBusMessaging;

namespace AspNetCoreServiceBusApi2
{
    public class ProcessData : IProcessData
    {
        public void Process(MyPayload myPayload)
        {
            DataServiceSimi.Data.Add(new Payload
            {
                Name = myPayload.Name,
                Goals = myPayload.Goals
            });
        }
    }
}

If only the ASP.NET Core application which sends messages is started, and a POST is called to for the topic API, a message will be sent to the Azure Service Bus topic. This can then be viewed in the portal.

If the API from the application which receives the topic subscriptions is started, the message will be sent and removed from the topic subscription.

Links:

https://docs.microsoft.com/en-us/azure/service-bus-messaging/

https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-dotnet-get-started-with-queues

https://www.nuget.org/packages/Microsoft.Azure.ServiceBus

Azure Service Bus Topologies

https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/multi-container-microservice-net-applications/integration-event-based-microservice-communications

Always subscribe to Dead-lettered messages when using an Azure Service Bus


Using Azure Service Bus Topics Subscription Filters in ASP.NET Core

$
0
0

This article shows how to implement Azure Service Bus filters for topic subscriptions used in an ASP.NET Core API application. The application uses the Microsoft.Azure.ServiceBus NuGet package for all the Azure Service Bus client logic.

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

Posts in this series:

Azure Service Bus Topic Sender

The topic sender from the previous post was changed to add a UserProperties item to the message called goals which will be filtered. Otherwise the sender is as before and sends the messages to the topic.

using Microsoft.Azure.ServiceBus;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Text;
using System.Threading.Tasks;

namespace ServiceBusMessaging
{
    public class ServiceBusTopicSender
    {
        private readonly TopicClient _topicClient;
        private readonly IConfiguration _configuration;
        private const string TOPIC_PATH = "mytopic";
        private readonly ILogger _logger;

        public ServiceBusTopicSender(IConfiguration configuration, 
            ILogger<ServiceBusTopicSender> logger)
        {
            _configuration = configuration;
            _logger = logger;
            _topicClient = new TopicClient(
                _configuration.GetConnectionString("ServiceBusConnectionString"),
                TOPIC_PATH
            );
        }
        
        public async Task SendMessage(MyPayload payload)
        {
            string data = JsonConvert.SerializeObject(payload);
            Message message = new Message(Encoding.UTF8.GetBytes(data));
            message.UserProperties.Add("goals", payload.Goals);

            try
            {
                await _topicClient.SendAsync(message);
            }
            catch (Exception e)
            {
                _logger.LogError(e.Message);
            }
        }
        
    }
}

It is not possible to add a subscription filter to the topic using the Azure portal. To do this you need to implement it in code, or used scripts, or the Azure CLI.

The RemoveDefaultFilters method checks if the default filter exists, and if it does it is removed. It does not remove the other filters.

private async Task RemoveDefaultFilters()
{
	try
	{
		var rules = await _subscriptionClient.GetRulesAsync();
		foreach(var rule in rules)
		{
			if(rule.Name == RuleDescription.DefaultRuleName)
			{
				await _subscriptionClient.RemoveRuleAsync(RuleDescription.DefaultRuleName);
			}
		}
		
	}
	catch (Exception ex)
	{
		_logger.LogWarning(ex.ToString());
	}
}

The AddFilters method adds the new filter, if it is not already added. The filter in this demo will use the goals user property from the message and only subscribe to messages with a value greater than 7.

private async Task AddFilters()
{
	try
	{
		var rules = await _subscriptionClient.GetRulesAsync();
		if(!rules.Any(r => r.Name == "GoalsGreaterThanSeven"))
		{
			var filter = new SqlFilter("goals > 7");
			await _subscriptionClient.AddRuleAsync("GoalsGreaterThanSeven", filter);
		}
	}
	catch (Exception ex)
	{
		_logger.LogWarning(ex.ToString());
	}
}

The filter methods are added to the PrepareFiltersAndHandleMessages method. This sets up the filters, or makes sure the filters are correct on the Azure Service Bus, and then registers itself to the topic subscription to receive the messages form its subscription.

public async Task PrepareFiltersAndHandleMessages()
{
	await RemoveDefaultFilters();
	await AddFilters();

	var messageHandlerOptions = new MessageHandlerOptions(ExceptionReceivedHandler)
	{
		MaxConcurrentCalls = 1,
		AutoComplete = false,
	};

	_subscriptionClient.RegisterMessageHandler(ProcessMessagesAsync, messageHandlerOptions);
}

The Azure Service Bus classes are added to the ASP.NET Core application in the Startup class. This adds the services to the IoC and initializes the message listener.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using ServiceBusMessaging;
using System.Threading.Tasks;

namespace AspNetCoreServiceBusApi2
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            ...
			
            services.AddSingleton<IServiceBusTopicSubscription, ServiceBusTopicSubscription>();
            services.AddTransient<IProcessData, ProcessData>();

        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            ...
			
            var busSubscription = app.ApplicationServices.GetService<IServiceBusTopicSubscription>();
            busSubscription.PrepareFiltersAndHandleMessages().GetAwaiter().GetResult();
        }
    }
}

When the applications are started, the API2 only receives messages which have a goal value greater than seven.

Links:

https://docs.microsoft.com/en-us/azure/service-bus-messaging/

https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-dotnet-get-started-with-queues

https://www.nuget.org/packages/Microsoft.Azure.ServiceBus

Azure Service Bus Topologies

https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/multi-container-microservice-net-applications/integration-event-based-microservice-communications

Always subscribe to Dead-lettered messages when using an Azure Service Bus

Using Entity Framework Core to process Azure Service Messages in ASP.NET Core

$
0
0

This article shows how to use Entity Framework Core together with an Azure Service Bus receiver in ASP.NET Core. This message handler is a singleton and so requires that an Entity Framework Core context inside this singleton and is not registered as a scoped service but created and disposed for each message event.

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

Posts in this series:

Processing the Azure Service Bus Messages

The ProcessData class is used to handle the messages in the ASP.NET Core application. The service uses an Entity Framework Core context to save the required data to a database.

using AspNetCoreServiceBusApi2.Model;
using Microsoft.Extensions.Configuration;
using ServiceBusMessaging;
using System;
using System.Threading.Tasks;

namespace AspNetCoreServiceBusApi2
{
    public class ProcessData : IProcessData
    {
        private IConfiguration _configuration;

        public ProcessData(IConfiguration configuration)
        {
            _configuration = configuration;
        }
        public async Task Process(MyPayload myPayload)
        {
            using (var payloadMessageContext = 
                new PayloadMessageContext(
                    _configuration.GetConnectionString("DefaultConnection")))
            {
                await payloadMessageContext.AddAsync(new Payload
                {
                    Name = myPayload.Name,
                    Goals = myPayload.Goals,
                    Created = DateTime.UtcNow
                });

                await payloadMessageContext.SaveChangesAsync();
            }
        }
    }
}

The services used to consume the Azure Service Bus are registered to the IoC (Inversion of Control) as singletons. Due to this, only singletons or transient services can be used. If we use the context as a singleton, we will end up having connection and pooling problems with the database.

services.AddSingleton<IServiceBusConsumer, ServiceBusConsumer>();
services.AddSingleton<IServiceBusTopicSubscription, ServiceBusTopicSubscription>();
services.AddSingleton<IProcessData, ProcessData>();

A PayloadMessageContext Entity Framework Core context was created for the Azure Service Bus message handling.

using Microsoft.EntityFrameworkCore;

namespace AspNetCoreServiceBusApi2.Model
{
    public class PayloadMessageContext : DbContext
    {
        private string _connectionString;

        public DbSet<Payload> Payloads { get; set; }
      
        public PayloadMessageContext(string connectionString)
        {
            _connectionString = connectionString;
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlite(_connectionString);
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            builder.Entity<Payload>().Property(n => n.Id).ValueGeneratedOnAdd();
            builder.Entity<Payload>().HasKey(m => m.Id); 
            base.OnModelCreating(builder); 
        } 
    }
}

The required NuGet packages were added to the project. This demo uses SQLite.

<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.0.0-preview4.19216.3" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.0.0-preview4.19216.3">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>

The service is then added in the Startup class as a singleton. The Entity Framework Core context used for the messaging is not registered here, because it is used inside the singleton instance and we do not want a context which is a singleton, because it will have problems with the database connections, and pooling. Instead a new context is created inside the service for each message event and disposed after. If you have a lot of messages, this would need to be optimized.

Now when the ASP.NET Core application receives messages, the singleton service context handles this messages, and saves the data to a database.

Links:

https://docs.microsoft.com/en-us/azure/service-bus-messaging/

https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-dotnet-get-started-with-queues

https://www.nuget.org/packages/Microsoft.Azure.ServiceBus

Azure Service Bus Topologies

https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/multi-container-microservice-net-applications/integration-event-based-microservice-communications

Always subscribe to Dead-lettered messages when using an Azure Service Bus

https://ml-software.ch/posts/stripe-api-with-asp-net-core-part-3

Certificate Authentication in ASP.NET Core 3.0

$
0
0

This article shows how Certificate Authentication can be implemented in ASP.NET Core 3.0. In this example, a shared self signed certificate is used to authenticate one application calling an API on a second ASP.NET Core application.

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

Posts in this series

Setting up the Server

Add the Certificate Authentication using the Microsoft.AspNetCore.Authentication.Certificate NuGet package to the server ASP.NET Core application.

This can also be added directly in the csproj file.

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Authentication.Certificate" 
      Version="3.0.0-preview6.19307.2" />
  </ItemGroup>

  <ItemGroup>
    <None Update="sts_dev_cert.pfx">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>

</Project>

The authentication can be added in the ConfigureServices method in the Startup class. This example was built using the ASP.NET Core documentation. The AddAuthentication extension method is used to define the default scheme as “Certificate” using the CertificateAuthenticationDefaults.AuthenticationScheme string. The AddCertificate method then adds the configuration for the certificate authentication. At present, all certificates are excepted which is not good and the MyCertificateValidationService class is used to do extra validation of the client certificate. If the validation fails, the request is failed and the request for the resource will be rejected.

public void ConfigureServices(IServiceCollection services)
{
	services.AddSingleton<MyCertificateValidationService>();

	services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
		.AddCertificate(options => // code from ASP.NET Core sample
		{
			options.AllowedCertificateTypes = CertificateTypes.All;
			options.Events = new CertificateAuthenticationEvents
			{
				OnCertificateValidated = context =>
				{
					var validationService =
						context.HttpContext.RequestServices.GetService<MyCertificateValidationService>();

					if (validationService.ValidateCertificate(context.ClientCertificate))
					{
						var claims = new[]
						{
							new Claim(ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer),
							new Claim(ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer)
						};

						context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
						context.Success();
					}
					else
					{
						context.Fail("invalid cert");
					}

					return Task.CompletedTask;
				}
			};
		});

	services.AddAuthorization();

	services.AddControllers();
}

The AddCertificateForwarding method is used so that the client header can be specified and how the certificate is to be loaded using the HeaderConverter option. When sending the certificate with the HttpClient using the default settings, the ClientCertificate was always be null. The X-ARR-ClientCert header is used to pass the client certificate, and the cert is passed as a string to work around this.

services.AddCertificateForwarding(options =>
{
	options.CertificateHeader = "X-ARR-ClientCert";
	options.HeaderConverter = (headerValue) =>
	{
		X509Certificate2 clientCertificate = null;
		if(!string.IsNullOrWhiteSpace(headerValue))
		{
			byte[] bytes = StringToByteArray(headerValue);
			clientCertificate = new X509Certificate2(bytes);
		}

		return clientCertificate;
	};
});

The Configure method then adds the middleware. UseCertificateForwarding is added before the UseAuthentication and the UseAuthorization.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	...
	
	app.UseRouting();

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

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

The MyCertificateValidationService is used to implement validation logic. Because we are using self signed certificates, we need to ensure that only our certificate can be used. We validate that the thumbprints of the client certificate and also the server one match, otherwise any certificate can be used and will be be enough to authenticate.

using System.IO;
using System.Security.Cryptography.X509Certificates;

namespace AspNetCoreCertificateAuthApi
{
    public class MyCertificateValidationService
    {
        public bool ValidateCertificate(X509Certificate2 clientCertificate)
        {
            var cert = new X509Certificate2(Path.Combine("sts_dev_cert.pfx"), "1234");
            if (clientCertificate.Thumbprint == cert.Thumbprint)
            {
                return true;
            }

            return false;
        }
    }
}

The API ValuesController is then secured using the Authorize attribute.

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

...

The ASP.NET Core server project is deployed in this example as an out of process application using kestrel. To use the service, a certificate is required. This is defined using the ClientCertificateMode.RequireCertificate option.

public static IWebHost BuildWebHost(string[] args)
  => WebHost.CreateDefaultBuilder(args)
  .UseStartup<Startup>()
  .ConfigureKestrel(options =>
  {
	var cert = new X509Certificate2(Path.Combine("sts_dev_cert.pfx"), "1234");
	options.ConfigureHttpsDefaults(o =>
	{
		o.ServerCertificate = cert;
		o.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
	});
  })
  .Build();

Implementing the HttpClient

The client of the API uses a HttpClient which was create using an instance of the IHttpClientFactory. This does not provide a way to define a handler for the HttpClient and so we use a HttpRequestMessage to add the Certificate to the “X-ARR-ClientCert” request header. The cert is added as a string using the GetRawCertDataString method.

private async Task<JArray> GetApiDataAsync()
{
	try
	{
		var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");

		var client = _clientFactory.CreateClient();

		var request = new HttpRequestMessage()
		{
			RequestUri = new Uri("https://localhost:44379/api/values"),
			Method = HttpMethod.Get,
		};

		request.Headers.Add("X-ARR-ClientCert", cert.GetRawCertDataString());
		var response = await client.SendAsync(request);

		if (response.IsSuccessStatusCode)
		{
			var responseContent = await response.Content.ReadAsStringAsync();
			var data = JArray.Parse(responseContent);

			return data;
		}

		throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
	}
	catch (Exception e)
	{
		throw new ApplicationException($"Exception {e}");
	}
}

If the correct certificate is sent to the server, the data will be returned. If no certificate is sent, or the wrong certificate, then a 403 will be returned. It would be nice if the IHttpClientFactory would have a way of defining a handler for the HttpClient. I also believe a non valid certificates should fail per default and not require extra validation for this. The AddCertificateForwarding should also not be required to use for a default HTTPClient client calling the service.

Certificate Authentication is great, and helps add another security layer which can be used together with other solutions. See the code and ASP.NET Core src code for further documentation and examples. Links underneath.

Links

https://docs.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-3.0

https://github.com/aspnet/AspNetCore/tree/master/src/Security/Authentication/Certificate/src

https://tools.ietf.org/html/rfc5246#section-7.4.4

Using Chained Certificates for Certificate Authentication in ASP.NET Core 3.0

$
0
0

This article shows how to create self signed certificates and use these for chained certificate authentication in ASP.NET Core. By using chained certificates, each client application can use a unique certificate which was created from a root CA directly, or an intermediate certificate which was created from the root CA. The clients can then be grouped or authenticated as required.

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

Posts in this series

Creating the Certificates

Creating the certificates is the hardest part in setting up this flow. A self signed Root CA Certificate is created using the New-SelfSignedCertificate powershell cmdlet. When creating this, please use a strong password, replace the demo one, do not just copy the code. It is important to add the KeyUsageProperty parameter and the KeyUsage parameter as shown.

Powershell commands:

New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" 
   -CertStoreLocation "cert:\LocalMachine\My" 
   -NotAfter (Get-Date).AddYears(20) 
   -FriendlyName "root_ca_dev_damienbod.com" 
   -KeyUsageProperty All 
   -KeyUsage CertSign, CRLSign, DigitalSignature

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath root_ca_dev_damienbod.crt

Install the root certificate in the trusted root of the host windows PC. If deploying this on Linux, different tools need to be used.

https://social.msdn.microsoft.com/Forums/SqlServer/en-US/5ed119ef-1704-4be4-8a4f-ef11de7c8f34/a-certificate-chain-processed-but-terminated-in-a-root-certificate-which-is-not-trusted-by-the

A self signed intermediate certificate can now be created from the root certificate. This is not required for all use cases, but you might need to create many certificates or need to activate, disable groups of certificates. The TextExtension parameter is required to set the pathlength in the basic constraints of the certificate.

The intermediate certificate can then be added to the trusted intermediate certificate in the windows host system.

Powershell commands:


$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint of the root..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my 
   -dnsname "intermediate_dev_damienbod.com" 
   -Signer $parentcert 
   -NotAfter (Get-Date).AddYears(20) 
   -FriendlyName "intermediate_dev_damienbod.com" 
   -KeyUsageProperty All 
   -KeyUsage CertSign, CRLSign, DigitalSignature
   -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\intermediate_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath intermediate_dev_damienbod.crt

A child certificate can be created from the intermediate certificate. This is the end entity and does not need to create more child certificates.

Powershell commands:

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the Intermediate certificate..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my 
  -dnsname "child_a_dev_damienbod.com" 
  -Signer $parentcert 
  -NotAfter (Get-Date).AddYears(20) 
  -FriendlyName "child_a_dev_damienbod.com"

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt

A child certificate can also be created from the root certificate directly. If you do not have many API clients, this could be used.

$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the root cert..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my 
  -dnsname "child_a_dev_damienbod.com" 
  -Signer $rootcert 
  -NotAfter (Get-Date).AddYears(20) 
  -FriendlyName "child_a_dev_damienbod.com"

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt

Server Setup

Now that the certificates are setup, the applications are created like in the previous blog. The AddAuthentication is configured to only accept CertificateTypes.Chained and the RevocationMode is set to NoCheck because we are using self signed chained certificates.

services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
	.AddCertificate(options => // code from ASP.NET Core sample
	{
		options.AllowedCertificateTypes = CertificateTypes.Chained;
		options.RevocationMode = X509RevocationMode.NoCheck;

		options.Events = new CertificateAuthenticationEvents
		{
			OnCertificateValidated = context =>
			{
				var validationService =
					context.HttpContext.RequestServices.GetService<MyCertificateValidationService>();

				if (validationService.ValidateCertificate(context.ClientCertificate))
				{
					var claims = new[]
					{
						new Claim(ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer),
						new Claim(ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer)
					};

					context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
					context.Success();
				}
				else
				{
					context.Fail("invalid cert");
				}

				return Task.CompletedTask;
			}
		};
	});

The application is configured in the program class to use the root certificate to validate the requests.

public static IWebHost BuildWebHost(string[] args)
	=> WebHost.CreateDefaultBuilder(args)
	.UseStartup<Startup>()
	.ConfigureKestrel(options =>
	{
		var cert = new X509Certificate2(Path.Combine("root_ca_dev_damienbod.pfx"), "1234");
		options.ConfigureHttpsDefaults(o =>
		{
			o.ServerCertificate = cert;
			o.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
		});
	})
	.Build();

The custom validation can then be added to the MyCertificateValidationService class. Here the client certificates are validated against the root certificate, or the intermediate certificate. This change be extended to use a dynamic list of Issuers and Subjects so that certificates can be activated or deactivated at runtime.

using System.IO;
using System.Security.Cryptography.X509Certificates;

namespace AspNetCoreCertificateAuthApi
{
    public class MyCertificateValidationService
    {
        private readonly X509Certificate2 rootCertificate = new X509Certificate2(Path.Combine("root_ca_dev_damienbod.pfx"), "1234");
        private readonly X509Certificate2 intermediateCertificate = new X509Certificate2(Path.Combine("child_a_dev_damienbod.pfx"), "1234");

        public bool ValidateCertificate(X509Certificate2 clientCertificate)
        {
            if (clientCertificate.Issuer == rootCertificate.Issuer || 
                clientCertificate.Issuer == intermediateCertificate.Subject)
            {
                return true;
            }

            return false;
        }
    }
}

Client Code

The client application is then setup to send the client certificate in the X-ARR-ClientCert request header. The server API is configured to use this to receive the certificates from the client. Now the chained certificates can be used to get access to the API.

private async Task<JArray> GetApiDataAsyncChained()
{
	try
	{
		// This is a child created from the root cert, must work
		//var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, "child_a_dev_damienbod.pfx"), "1234");

		// This is a child created from the intermediate certificate 
		// which is a cert created from the root cert, must work
		var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, "child_b_from_a_dev_damienbod.pfx"), "1234");

		// This is a NOT child of the root cert or the intermediate certificate
		// , must fail
		//var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");

		var client = _clientFactory.CreateClient();

		var request = new HttpRequestMessage()
		{
			RequestUri = new Uri("https://localhost:44378/api/values"),
			Method = HttpMethod.Get,
		};

		request.Headers.Add("X-ARR-ClientCert", cert.GetRawCertDataString());
		var response = await client.SendAsync(request);

		if (response.IsSuccessStatusCode)
		{
			var responseContent = await response.Content.ReadAsStringAsync();
			var data = JArray.Parse(responseContent);

			return data;
		}

		throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
	}
	catch (Exception e)
	{
		throw new ApplicationException($"Exception {e}");
	}
}

See the github code for the full working example. By using chained certificates, new certificates can be created on the fly for usage with new API clients, and the root certificate does not need to be deployed. This would become really useful when securing APIs which are not always connected to the internet, or with distributed devices.

Links

https://docs.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-3.0

https://docs.microsoft.com/en-us/dotnet/framework/wcf/feature-details/working-with-certificates

https://docs.microsoft.com/en-us/dotnet/framework/wcf/feature-details/how-to-create-temporary-certificates-for-use-during-development

HowTo: Create Self-Signed Certificates with PowerShell

HTTPS and X509 certificates in .NET Part 2: creating self-signed certificates

https://www.humankode.com/asp-net-core/develop-locally-with-https-self-signed-certificates-and-asp-net-core

https://damienbod.com/2018/09/21/deploying-an-asp-net-core-application-to-windows-iis/

https://docs.microsoft.com/en-us/powershell/module/pkiclient/new-selfsignedcertificate?view=win10-ps

https://github.com/damienbod/IdentityServer4AspNetCoreIdentityTemplate#using-powershell-to-create-the-self-signed-certs

Using client certificates in .NET part 5: working with client certificates in a web project

https://stackoverflow.com/questions/42623080/how-to-validate-a-certificate-chain-from-a-specific-root-ca-in-c-sharp

https://docs.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-3.0

https://social.msdn.microsoft.com/Forums/SqlServer/en-US/5ed119ef-1704-4be4-8a4f-ef11de7c8f34/a-certificate-chain-processed-but-terminated-in-a-root-certificate-which-is-not-trusted-by-the

https://tools.ietf.org/html/rfc3280.html

https://github.com/aspnet/AspNetCore/tree/master/src/Security/Authentication/Certificate/src

https://tools.ietf.org/html/rfc5246#section-7.4.4

An alternative way to build and bundle Javascript, CSS in ASP.NET Core MVC and Razor Page projects

$
0
0

This article shows how Javascript packages, files, CSS files could be built and bundled in an ASP.NET Core MVC or Razor Page application. The Javascript packages are loaded using npm in which most Javascript projects are deployed. No CDNs are used, only local files so that all external URLs, non self URL links can be completely blocked. This can help in reducing security risks in your application. By using npm, it makes it really simple to update and maintain the UI packages.

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

Npm is used to manage the frontend packages. To use this, download Node.js and install. Npm will then be ready to use.

The npm package.json contains the packages required for the ASP.NET Core application. For most applications, I use Bootstrap 4. jQuery is used in all of my ASP.NET Core MVC, Razor Page projects as this is probably the most stable Javascript package which exists. This has been stable for a very long time, and is easy to update. I think the cost of updating and maintaining Javascript script packages is underestimated when creating a new technoligy stack. query-validation, jquery-validation-unobtrusive and jquery-ajax-unobtrusive are used for the ASP.NET Core validation and the html attributes which setup the ajax calls, or partial requests.

{
  "version": "1.0.0",
  "name": "asp.net",
  "private": true,
  "devDependencies": {
    "bootstrap": "4.3.1",
    "jquery": "3.4.1",
    "jquery-validation": "1.19.1",
    "jquery-validation-unobtrusive": "3.2.11",
    "jquery-ajax-unobtrusive": "3.2.6",
    "popper.js": "^1.15.0"
  },
  "dependencies": {}
}

The bundleconfig.json file is used to bundle the packages and minify and so on. This file must be configured to match the packages which you use.

// Configure bundling and minification for the project.
// More info at https://go.microsoft.com/fwlink/?LinkId=808241
[
  {
    "outputFileName": "wwwroot/css/site.min.css",
    // An array of relative input file paths. Globbing patterns supported
    "inputFiles": [
      "wwwroot/css/site.css"
    ],
    "minify": { "enabled": true }
  },
  {
    "outputFileName": "wwwroot/js/site.min.js",
    "inputFiles": [
      "wwwroot/js/site.js"
    ],
    // Optionally specify minification options
    "minify": {
      "enabled": true,
      "renameLocals": true
    },
    // Optinally generate .map file
    "sourceMap": false
  },
  // Vendor CSS
  {
    "outputFileName": "wwwroot/css/vendor.min.css",
    "inputFiles": [
      "node_modules/bootstrap/dist/css/bootstrap.min.css"
    ],
    "minify": { "enabled": true }
  },
  // Vendor JS
  {
    "outputFileName": "wwwroot/js/vendor.min.js",
    "inputFiles": [
      "node_modules/jquery/dist/jquery.min.js",
      "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
    ],
    "minify": {
      "enabled": true,
      "renameLocals": true
    },
    "sourceMap": false
  },
  // Vendor Validation JS
  {
    "outputFileName": "wwwroot/js/vendor-validation.min.js",
    "inputFiles": [
      "node_modules/jquery-validation/dist/jquery.validate.min.js",
      "node_modules/jquery-validation/dist/additional-methods.min.js",
      "node_modules/jquery-validation-unobtrusive/dist/jquery.validate.unobtrusive.min.js",
      "node_modules/jquery-ajax-unobtrusive/dist/jquery.unobtrusive-ajax.min.js"
    ],
    "minify": {
      "enabled": true,
      "renameLocals": true
    },
    "sourceMap": false
  }
]

The bundles are created and saved to the wwwroot.

The csproj file is setup to use the BuildBundlerMinifier, so that the bundles are created. The npm packages are also downloaded, if the node_modules don’t exist. This project is an ASP.NET Core 3.0 project.

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>


  <ItemGroup>
    <PackageReference Include="BuildBundlerMinifier" Version="2.9.406" />
  </ItemGroup>

  <Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('node_modules') ">
    <!-- Ensure Node.js is installed -->
    <Exec Command="node --version" ContinueOnError="true">
      <Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
    </Exec>
    <Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />
    <Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
    <Exec WorkingDirectory="" Command="npm install" />
  </Target>
  
</Project>

The _Layout.cshtml razor view contains the links to the CSS and the Javscript bundles. Depending on your ASP.NET Core MVC project, Razor Page project, the vendor-validation.min.js file could be moved to a separate view, which is only used for the forms or the ajax views. In this layout no external CDNs are used.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - AspNetCoreInjectConfigurationRazor</title>

    <link rel="stylesheet" href="~/css/vendor.min.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/css/fontawesome-free-5.4.1-web/css/all.min.css" asp-append-version="true" />
</head>
<body>

    <div class="container body-content">
        @RenderBody()
    </div>

    <script src="~/js/vendor.min.js" asp-append-version="true"></script>
    <script src="~/js/vendor-validation.min.js" asp-append-version="true"></script>
    <script src="~/js/site.min.js" asp-append-version="true"></script>

    @RenderSection("scripts", required: false)
</body>
</html>

Updating packages

I use npm-check-updates to update my npm packages. Install this as documented in the package docs.

For the command line, ncu -u can be used to update the npm packages. These needs to be executed in the same folder where the packages.json file is.

ncu -u

You can then update the bundleconfig.json to match the updated npm packages. Per default, jQuery and Bootstrap 4 do not change so often, so usually nothing is required here.

After trying many different ways of updating UI packages and CSS in different projects for many different clients, this solution had the least effort to update and maintain the Javascript and CSS.

Links

https://docs.microsoft.com/en-us/aspnet/core/client-side/bundling-and-minification

https://nodejs.org/en/download/

https://www.npmjs.com/package/npm-check-updates

ASP.NET Core Identity with Fido2 WebAuthn MFA

$
0
0

This article shows how Fido2 WebAuthn could be used as 2FA and integrated into an ASP.NET Core Identity application. The Fido2 WebAuthn is implemented using the fido2-net-lib Nuget package, and demo code created by Anders Åberg. The application is implemented using ASP.NET Core 3.0 with Identity. For information about Fido2 and WebAuthn, please refer to the links at the bottom.

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

Creating the ASP.NET Core Application with Identity

The application was created using the ASP.NET Core 3.0 Razor Page template with Identity (Individual User Accounts).

The following NuGet packages were added. Fido2 is used to implement the Fido2 2FA. Microsoft.AspNetCore.Mvc.NewtonsoftJson is required for the serialization of the Fido requests, responses.

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <UserSecretsId>aspnet-AspNetCoreIdentityFido2Mfa-590BDC19-E82D-4E23-9F2E-346DA31B5198</UserSecretsId>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Fido2" Version="1.0.1" />
    <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.0.0-preview7.19365.7" />
    <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.0.0-preview7.19365.7" />
    <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.0.0-preview7.19365.7" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.0.0-preview7.19365.7" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.0.0-preview7.19362.6" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.0.0-preview7.19362.6" />
    <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.0.0-preview7.19362.4" />
    <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.0.0-preview7-19378-04" />
  </ItemGroup>

</Project>

The Identity UI Pages were then scaffolded into the project.

Adding the 2FA using Fido2

Fido2 with the YubiKey 5 Series is used as a 2FA for Identity. For this to work, the user needs to be able to register and activate the Fido2 2FA, then the user can login using the Fido2 2FA and identity. The application also needs a way to deactivate or remove the 2FA.

The demo project from https://github.com/abergs/fido2-net-lib was used to implement the Fido2 logic and also the nuget package. See the docs in the repo.

First the Javascript scripts were added to the wwwroot. mfa.login.js and mfa.register.js plus the 2 required script were added. The scripts were then changed to match the Razor Page templates and the identity requirements.

These scripts depend on other npm packages, CDNs. These were added to the _Layout.cshtml.

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - AspNetCoreIdentityFido2Mfa</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" />

    <link href="https://fonts.googleapis.com/css?family=Work+Sans" rel="stylesheet">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js" integrity="sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4" crossorigin="anonymous"></script>
    <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/sweetalert2"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/limonte-sweetalert2/6.10.1/sweetalert2.min.css" />
    <script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>

</head>

Register Fido 2FA

A Fido2Mfa Razor Page view was created and added to the Identity/Account/Manage folder. The page uses the logged in user. The page can only be accessed when the user has already logged in. The page is used to activate and register the Fido2 device.

@page "/Fido2Mfa/{handler?}"
@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager
@model AspNetCoreIdentityFido2Mfa.Areas.Identity.Pages.Account.Manage.MfaModel
@{
    Layout = "_Layout.cshtml";
    ViewData["Title"] = "Two-factor authentication (2FA)";
    ViewData["ActivePage"] = ManageNavPages.Fido2Mfa;
}

<h4>@ViewData["Title"]</h4>
<div class="section">
    <div class="container">
        <h1 class="title is-1">2FA/MFA</h1>
        <div class="content"><p>This is scenario where we just want to use FIDO as the MFA. The user register and logins with their username and password. For demo purposes, we trigger the MFA registering on sign up.</p></div>
        <div class="notification is-danger" style="display:none">
            Please note: Your browser does not seem to support WebAuthn yet. <a href="https://caniuse.com/#search=webauthn" target="_blank">Supported browsers</a>
        </div>

        <div class="columns">
            <div class="column is-4">

                <h3 class="title is-3">Add a Fido2 MFA</h3>
                <form action="/Fido2Mfa" method="post" id="register">
                    <div class="field">
                        <label class="label">Username</label>
                        <div class="control has-icons-left has-icons-right">
                            <input class="form-control" type="text" readonly placeholder="email" value="@User.Identity.Name" name="username" required>
                        </div>
                    </div>

                    <div class="field" style="margin-top:10px;">
                        <div class="control">
                            <button class="btn btn-primary">Add Fido2 MFA</button>
                        </div>
                    </div>
                </form>
            </div>
        </div>


    </div>
</div>

<script src="~/js/helpers.js"></script>
<script src="~/js/instant.js"></script>
<script src="~/js/mfa.register.js"></script>

The mfa.register.js uses the RegisterFido2Controller to make the Credential Options and to make the Credential. If successful, 2FA is enabled for the Identity using the Identity UserManager.

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Fido2NetLib.Objects;
using Fido2NetLib;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using static Fido2NetLib.Fido2;
using System.IO;
using Microsoft.AspNetCore.Identity;

namespace AspNetCoreIdentityFido2Mfa
{

    [Route("api/[controller]")]
    public class RegisterFido2Controller : Controller
    {
        private Fido2 _lib;
        public static IMetadataService _mds;
        private string _origin;
        private readonly Fido2Storage _fido2Storage;
        private readonly UserManager<IdentityUser> _userManager;

        public RegisterFido2Controller(IConfiguration config, Fido2Storage fido2Storage, UserManager<IdentityUser> userManager)
        {
            _userManager = userManager;
            _fido2Storage = fido2Storage;
            var MDSAccessKey = config["fido2:MDSAccessKey"];
            var MDSCacheDirPath = config["fido2:MDSCacheDirPath"] ?? Path.Combine(Path.GetTempPath(), "fido2mdscache"); 
            _mds = string.IsNullOrEmpty(MDSAccessKey) ? null : MDSMetadata.Instance(MDSAccessKey, MDSCacheDirPath);
            if (null != _mds)
            {
                if (false == _mds.IsInitialized())
                    _mds.Initialize().Wait();
            }
            _origin = config["fido2:origin"];
            if(_origin == null)
            {
                _origin = "https://localhost:44388";
            }

            var domain = config["fido2:serverDomain"];
            if (domain == null)
            {
                domain = "localhost";
            }

            _lib = new Fido2(new Fido2Configuration()
            {
                ServerDomain = domain,
                ServerName = "Fido2IdentityMfa",
                Origin = _origin,
                // Only create and use Metadataservice if we have an acesskey
                MetadataService = _mds,
                TimestampDriftTolerance = config.GetValue<int>("fido2:TimestampDriftTolerance")
            });
        }

        private string FormatException(Exception e)
        {
            return string.Format("{0}{1}", e.Message, e.InnerException != null ? " (" + e.InnerException.Message + ")" : "");
        }

        [HttpPost]
        [Route("/makeCredentialOptions")]
        public async Task<JsonResult> MakeCredentialOptions([FromForm] string username, [FromForm] string displayName, [FromForm] string attType, [FromForm] string authType, [FromForm] bool requireResidentKey, [FromForm] string userVerification)
        {
            try
            {
                if (string.IsNullOrEmpty(username))
                {
                    username = $"{displayName} (Usernameless user created at {DateTime.UtcNow})";
                }

                var identityUser = await _userManager.FindByEmailAsync(username);
                var user = new Fido2User
                {
                    DisplayName = identityUser.UserName,
                    Name = identityUser.UserName,
                    Id = Encoding.UTF8.GetBytes(identityUser.UserName) // byte representation of userID is required
                };

                // 2. Get user existing keys by username
                var items = await _fido2Storage.GetCredentialsByUsername(identityUser.UserName);
                var existingKeys = new List<PublicKeyCredentialDescriptor>();
                foreach(var publicKeyCredentialDescriptor in items)
                {
                    existingKeys.Add(publicKeyCredentialDescriptor.Descriptor);
                }

                // 3. Create options
                var authenticatorSelection = new AuthenticatorSelection
                {
                    RequireResidentKey = requireResidentKey,
                    UserVerification = userVerification.ToEnum<UserVerificationRequirement>()
                };

                if (!string.IsNullOrEmpty(authType))
                    authenticatorSelection.AuthenticatorAttachment = authType.ToEnum<AuthenticatorAttachment>();

                var exts = new AuthenticationExtensionsClientInputs() { Extensions = true, UserVerificationIndex = true, Location = true, UserVerificationMethod = true, BiometricAuthenticatorPerformanceBounds = new AuthenticatorBiometricPerfBounds { FAR = float.MaxValue, FRR = float.MaxValue } };

                var options = _lib.RequestNewCredential(user, existingKeys, authenticatorSelection, attType.ToEnum<AttestationConveyancePreference>(), exts);

                // 4. Temporarily store options, session/in-memory cache/redis/db
                HttpContext.Session.SetString("fido2.attestationOptions", options.ToJson());

                // 5. return options to client
                return Json(options);
            }
            catch (Exception e)
            {
                return Json(new CredentialCreateOptions { Status = "error", ErrorMessage = FormatException(e) });
            }
        }

        [HttpPost]
        [Route("/makeCredential")]
        public async Task<JsonResult> MakeCredential([FromBody] AuthenticatorAttestationRawResponse attestationResponse)
        {
            try
            {
                // 1. get the options we sent the client
                var jsonOptions = HttpContext.Session.GetString("fido2.attestationOptions");
                var options = CredentialCreateOptions.FromJson(jsonOptions);

                // 2. Create callback so that lib can verify credential id is unique to this user
                IsCredentialIdUniqueToUserAsyncDelegate callback = async (IsCredentialIdUniqueToUserParams args) =>
                {
                    var users = await _fido2Storage.GetUsersByCredentialIdAsync(args.CredentialId);
                    if (users.Count > 0) return false;

                    return true;
                };

                // 2. Verify and make the credentials
                var success = await _lib.MakeNewCredentialAsync(attestationResponse, options, callback);

                // 3. Store the credentials in db
                await _fido2Storage.AddCredentialToUser(options.User, new FidoStoredCredential
                {
                    Username = options.User.Name,
                    Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId),
                    PublicKey = success.Result.PublicKey,
                    UserHandle = success.Result.User.Id,
                    SignatureCounter = success.Result.Counter,
                    CredType = success.Result.CredType,
                    RegDate = DateTime.Now,
                    AaGuid = success.Result.Aaguid
                });

                // 4. return "ok" to the client

                var user = await _userManager.GetUserAsync(User);
                if (user == null)
                {
                    return Json(new CredentialMakeResult { Status = "error", ErrorMessage = $"Unable to load user with ID '{_userManager.GetUserId(User)}'." });
                }

                await _userManager.SetTwoFactorEnabledAsync(user, true);
                var userId = await _userManager.GetUserIdAsync(user);

                return Json(success);
            }
            catch (Exception e)
            {
                return Json(new CredentialMakeResult { Status = "error", ErrorMessage = FormatException(e) });
            }
        }
    }
}

The RegisterFido2Controller class uses the FidoStoredCredential entity and the Fido2Storage to persist the Fido2 data to the SQL database using Entity Framework Core. The PublicKeyCredentialDescriptor property is persisted as a Json string.

public class FidoStoredCredential
{
	public string Username { get; set; }
	public byte[] UserId { get; set; }
	public byte[] PublicKey { get; set; }
	public byte[] UserHandle { get; set; }
	public uint SignatureCounter { get; set; }
	public string CredType { get; set; }
	public DateTime RegDate { get; set; }
	public Guid AaGuid { get; set; }

	[NotMapped]
	public PublicKeyCredentialDescriptor Descriptor
	{
		get { return string.IsNullOrWhiteSpace(DescriptorJson) ? null : JsonConvert.DeserializeObject<PublicKeyCredentialDescriptor>(DescriptorJson); }
		set { DescriptorJson = JsonConvert.SerializeObject(value); }
	}
	public string DescriptorJson { get; set; }
}

Teh entity is added to the ApplicationDbContext. The Username which is the unique email, is used as a key.

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace AspNetCoreIdentityFido2Mfa.Data
{
    public class ApplicationDbContext : IdentityDbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        public DbSet<FidoStoredCredential> FidoStoredCredential { get; set; }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            builder.Entity<FidoStoredCredential>().HasKey(m => m.Username);

            base.OnModelCreating(builder);
        }
    }
}

The FidoStoredCredential class can now be used to interact with the database.

using AspNetCoreIdentityFido2Mfa.Data;
using Fido2NetLib;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AspNetCoreIdentityFido2Mfa
{
    public class Fido2Storage
    {
       private readonly ApplicationDbContext _applicationDbContext;

        public Fido2Storage(ApplicationDbContext applicationDbContext)
        {
            _applicationDbContext = applicationDbContext;
        }

        public async Task<List<FidoStoredCredential>> GetCredentialsByUsername(string username)
        {
            return await _applicationDbContext.FidoStoredCredential.Where(c => c.Username == username).ToListAsync();
        }

        public async Task RemoveCredentialsByUsername(string username)
        {
            var item = await _applicationDbContext.FidoStoredCredential.Where(c => c.Username == username).FirstOrDefaultAsync();
            if(item != null)
            {
                _applicationDbContext.FidoStoredCredential.Remove(item);
                await _applicationDbContext.SaveChangesAsync();
            }
        }

        public async Task<FidoStoredCredential> GetCredentialById(byte[] id)
        {
            var credentialIdString = Base64Url.Encode(id);
            //byte[] credentialIdStringByte = Base64Url.Decode(credentialIdString);

            var cred = await _applicationDbContext.FidoStoredCredential
                .Where(c => c.DescriptorJson.Contains(credentialIdString)).FirstOrDefaultAsync();

            return cred;
        }

        public Task<List<FidoStoredCredential>> GetCredentialsByUserHandleAsync(byte[] userHandle)
        {
            return Task.FromResult(_applicationDbContext.FidoStoredCredential.Where(c => c.UserHandle.SequenceEqual(userHandle)).ToList());
        }

        public async Task UpdateCounter(byte[] credentialId, uint counter)
        {
            var credentialIdString = Base64Url.Encode(credentialId);
            //byte[] credentialIdStringByte = Base64Url.Decode(credentialIdString);

            var cred = await _applicationDbContext.FidoStoredCredential
                .Where(c => c.DescriptorJson.Contains(credentialIdString)).FirstOrDefaultAsync();

            cred.SignatureCounter = counter;
            await _applicationDbContext.SaveChangesAsync();
        }

        public async Task AddCredentialToUser(Fido2User user, FidoStoredCredential credential)
        {
            credential.UserId = user.Id;
            _applicationDbContext.FidoStoredCredential.Add(credential);
            await _applicationDbContext.SaveChangesAsync();
        }

        public async Task<List<Fido2User>> GetUsersByCredentialIdAsync(byte[] credentialId)
        {
            var credentialIdString = Base64Url.Encode(credentialId);
            //byte[] credentialIdStringByte = Base64Url.Decode(credentialIdString);

            var cred = await _applicationDbContext.FidoStoredCredential
                .Where(c => c.DescriptorJson.Contains(credentialIdString)).FirstOrDefaultAsync();

            if (cred == null)
            {
                return new List<Fido2User>();
            }

            return await _applicationDbContext.Users
                    .Where(u => Encoding.UTF8.GetBytes(u.UserName)
                    .SequenceEqual(cred.UserId))
                    .Select(u => new Fido2User
                    {
                        DisplayName = u.UserName,
                        Name = u.UserName,
                        Id = Encoding.UTF8.GetBytes(u.UserName) // byte representation of userID is required
                    }).ToListAsync();
        }
    }
}

Now the user can register and activate a Fido2 2FA with Identity. Start the application and login. Click your email, and then Two-Factor authentication.

Click Add Fido2 MFA

Click Add Fido2 MFA. Now you can use your hardware key to complete the request.

Login with Fido2

The next step is to implement the login with Fido2 2FA. A code if statement to use the Fido 2FA is added to the Account login page. The Fido2Storage instance is added and this is user to check if a Fido2 registration exists for this user. If so, after a successful login, the user is redirected to the second step at the LoginFido2Mfa Razor Page.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;

namespace AspNetCoreIdentityFido2Mfa.Areas.Identity.Pages.Account
{
    [AllowAnonymous]
    public class LoginModel : PageModel
    {
        private readonly UserManager<IdentityUser> _userManager;
        private readonly SignInManager<IdentityUser> _signInManager;
        private readonly ILogger<LoginModel> _logger;
        private readonly IEmailSender _emailSender;
        private readonly Fido2Storage _fido2Storage;

        public LoginModel(SignInManager<IdentityUser> signInManager, 
            ILogger<LoginModel> logger,
            UserManager<IdentityUser> userManager,
            IEmailSender emailSender,
            Fido2Storage fido2Storage)
        {
            _fido2Storage = fido2Storage;
            _userManager = userManager;
            _signInManager = signInManager;
            _emailSender = emailSender;
            _logger = logger;
        }

        public async Task<IActionResult> OnPostAsync(string returnUrl = null)
        {
            returnUrl = returnUrl ?? Url.Content("~/");

            if (ModelState.IsValid)
            {
                // This doesn't count login failures towards account lockout
                // To enable password failures to trigger account lockout, set lockoutOnFailure: true
                var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true);
                if (result.Succeeded)
                {
                    _logger.LogInformation("User logged in.");
                    return LocalRedirect(returnUrl);
                }
                if (result.RequiresTwoFactor)
                {
                    var fido2ItemExistsForUser = await _fido2Storage.GetCredentialsByUsername(Input.Email);
                    if (fido2ItemExistsForUser.Count > 0)
                    {
                        return RedirectToPage("./LoginFido2Mfa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
                    }
                    else
                    {
                        return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
                    }
                }
                
                if (result.IsLockedOut)
                {
                    _logger.LogWarning("User account locked out.");
                    return RedirectToPage("./Lockout");
                }
                else
                {
                    ModelState.AddModelError(string.Empty, "Invalid login attempt.");
                    return Page();
                }
            }

            // If we got this far, something failed, redisplay form
            return Page();
        }
    }
}

The LoginFido2Mfa Razor Page uses the mfa.login.js to implement the Fido2 logic.

@page 
@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager
@model AspNetCoreIdentityFido2Mfa.Areas.Identity.Pages.Account.MfaModel
@{
    ViewData["Title"] = "Login with Fido2 MFA";
}

<h4>@ViewData["Title"]</h4>
<div class="section">
    <div class="container">
        <h1 class="title is-1">2FA/MFA</h1>
        <div class="content"><p>This is scenario where we just want to use FIDO as the MFA. The user register and logins with their username and password. For demo purposes, we trigger the MFA registering on sign up.</p></div>
        <div class="notification is-danger" style="display:none">
            Please note: Your browser does not seem to support WebAuthn yet. <a href="https://caniuse.com/#search=webauthn" target="_blank">Supported browsers</a>
        </div>

        <div class="columns">
            <div class="column is-4">

                <h3 class="title is-3">Fido2 2FA</h3>
                <form action="/LoginFido2Mfa" method="post" id="signin">

                    <div class="field">
                        <div class="control">
                            <button class="btn btn-primary">2FA with Fido2 device</button>
                        </div>
                    </div>
                </form>
            </div>
        </div>

    </div>
</div>

<script src="~/js/helpers.js"></script>
<script src="~/js/instant.js"></script>
<script src="~/js/mfa.login.js"></script>

The mfa.login.js script uses the SignInFidoController to complete the login. This controller has two methods, AssertionOptionsPost and MakeAssertion.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Fido2NetLib.Objects;
using Fido2NetLib;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using static Fido2NetLib.Fido2;
using System.IO;
using Microsoft.AspNetCore.Identity;

namespace AspNetCoreIdentityFido2Mfa
{

    [Route("api/[controller]")]
    public class SignInFidoController : Controller
    {
        private Fido2 _lib;
        public static IMetadataService _mds;
        private string _origin;
        private readonly Fido2Storage _fido2Storage;
        private readonly UserManager<IdentityUser> _userManager;
        private readonly SignInManager<IdentityUser> _signInManager;

        public SignInFidoController(IConfiguration config,
            Fido2Storage fido2Storage,
            UserManager<IdentityUser> userManager,
            SignInManager<IdentityUser> signInManager)
        {
            _signInManager = signInManager;
            _userManager = userManager;
            _fido2Storage = fido2Storage;
            var MDSAccessKey = config["fido2:MDSAccessKey"];
            var MDSCacheDirPath = config["fido2:MDSCacheDirPath"] ?? Path.Combine(Path.GetTempPath(), "fido2mdscache");
            _mds = string.IsNullOrEmpty(MDSAccessKey) ? null : MDSMetadata.Instance(MDSAccessKey, MDSCacheDirPath);
            if (null != _mds)
            {
                if (false == _mds.IsInitialized())
                    _mds.Initialize().Wait();
            }
            _origin = config["fido2:origin"];
            _lib = new Fido2(new Fido2Configuration()
            {
                ServerDomain = config["fido2:serverDomain"],
                ServerName = "Fido2 test",
                Origin = _origin,
                // Only create and use Metadataservice if we have an acesskey
                MetadataService = _mds,
                TimestampDriftTolerance = config.GetValue<int>("fido2:TimestampDriftTolerance")
            });
        }

        private string FormatException(Exception e)
        {
            return string.Format("{0}{1}", e.Message, e.InnerException != null ? " (" + e.InnerException.Message + ")" : "");
        }

        [HttpPost]
        [Route("/assertionOptions")]
        public async Task<ActionResult> AssertionOptionsPost([FromForm] string username, [FromForm] string userVerification)
        {
            try
            {
                var identityUser = await _signInManager.GetTwoFactorAuthenticationUserAsync();
                if (identityUser == null)
                {
                    throw new InvalidOperationException($"Unable to load two-factor authentication user.");
                }

                var existingCredentials = new List<PublicKeyCredentialDescriptor>();

                if (!string.IsNullOrEmpty(identityUser.UserName))
                {
                    
                    var user = new Fido2User
                    {
                        DisplayName = identityUser.UserName,
                        Name = identityUser.UserName,
                        Id = Encoding.UTF8.GetBytes(identityUser.UserName) // byte representation of userID is required
                    };

                    if (user == null) throw new ArgumentException("Username was not registered");

                    // 2. Get registered credentials from database
                    var items = await _fido2Storage.GetCredentialsByUsername(identityUser.UserName);
                    existingCredentials = items.Select(c => c.Descriptor).ToList();
                }

                var exts = new AuthenticationExtensionsClientInputs() { SimpleTransactionAuthorization = "FIDO", GenericTransactionAuthorization = new TxAuthGenericArg { ContentType = "text/plain", Content = new byte[] { 0x46, 0x49, 0x44, 0x4F } }, UserVerificationIndex = true, Location = true, UserVerificationMethod = true };

                // 3. Create options
                var uv = string.IsNullOrEmpty(userVerification) ? UserVerificationRequirement.Discouraged : userVerification.ToEnum<UserVerificationRequirement>();
                var options = _lib.GetAssertionOptions(
                    existingCredentials,
                    uv,
                    exts
                );

                // 4. Temporarily store options, session/in-memory cache/redis/db
                HttpContext.Session.SetString("fido2.assertionOptions", options.ToJson());

                // 5. Return options to client
                return Json(options);
            }

            catch (Exception e)
            {
                return Json(new AssertionOptions { Status = "error", ErrorMessage = FormatException(e) });
            }
        }

        [HttpPost]
        [Route("/makeAssertion")]
        public async Task<JsonResult> MakeAssertion([FromBody] AuthenticatorAssertionRawResponse clientResponse)
        {
            try
            {
                // 1. Get the assertion options we sent the client
                var jsonOptions = HttpContext.Session.GetString("fido2.assertionOptions");
                var options = AssertionOptions.FromJson(jsonOptions);

                // 2. Get registered credential from database
                var creds = await _fido2Storage.GetCredentialById(clientResponse.Id);

                if (creds == null)
                {
                    throw new Exception("Unknown credentials");
                }

                // 3. Get credential counter from database
                var storedCounter = creds.SignatureCounter;

                // 4. Create callback to check if userhandle owns the credentialId
                IsUserHandleOwnerOfCredentialIdAsync callback = async (args) =>
                {
                    var storedCreds = await _fido2Storage.GetCredentialsByUserHandleAsync(args.UserHandle);
                    return storedCreds.Exists(c => c.Descriptor.Id.SequenceEqual(args.CredentialId));
                };

                // 5. Make the assertion
                var res = await _lib.MakeAssertionAsync(clientResponse, options, creds.PublicKey, storedCounter, callback);

                // 6. Store the updated counter
                await _fido2Storage.UpdateCounter(res.CredentialId, res.Counter);

                // complete sign-in
                var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
                if (user == null)
                {
                    throw new InvalidOperationException($"Unable to load two-factor authentication user.");
                }
                
                var result = await _signInManager.TwoFactorSignInAsync("FIDO2", string.Empty, false, false);

                // 7. return OK to client
                return Json(res);
            }
            catch (Exception e)
            {
                return Json(new AssertionVerificationResult { Status = "error", ErrorMessage = FormatException(e) });
            }
        }
    }
}

If the Fido2 login is successful, the signInManager.TwoFactorSignInAsync method is used to complete the 2FA in Identity.

This requires an implementation of the IUserTwoFactorTokenProvider to complete the login. The Fifo2UserTwoFactorTokenProvider implements the IUserTwoFactorTokenProvider interface and just returns true.

using Microsoft.AspNetCore.Identity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace AspNetCoreIdentityFido2Mfa
{
    public class Fifo2UserTwoFactorTokenProvider : IUserTwoFactorTokenProvider<IdentityUser>
    {
        public Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<IdentityUser> manager, IdentityUser user)
        {
            return Task.FromResult(true);
        }

        public Task<string> GenerateAsync(string purpose, UserManager<IdentityUser> manager, IdentityUser user)
        {
            return Task.FromResult("fido2");
        }

        public Task<bool> ValidateAsync(string purpose, string token, UserManager<IdentityUser> manager, IdentityUser user)
        {
            return Task.FromResult(true);
        }
    }
}

For all of this to work, the services and everything needs to be configured in the startup class. The AddControllers method is used to add the services for the Fido2 controllers as well as the extension method AddNewtonsoftJson(). The AddTokenProvider method is used to add the Fido2 2FA provider. Fido2Storage is added as a scoped service.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.EntityFrameworkCore;
using AspNetCoreIdentityFido2Mfa.Data;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace AspNetCoreIdentityFido2Mfa
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {

            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection")));
            services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddTokenProvider<Fifo2UserTwoFactorTokenProvider>("FIDO2");


            services.AddControllers()
               .AddNewtonsoftJson();

            services.AddRazorPages();

            services.AddScoped<Fido2Storage>();
            // Adds a default in-memory implementation of IDistributedCache.
            services.AddDistributedMemoryCache();
            services.AddSession(options =>
            {
                // Set a short timeout for easy testing.
                options.IdleTimeout = TimeSpan.FromMinutes(2);
                options.Cookie.HttpOnly = true;
                options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None;
            });

        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

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

            app.UseSession();

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

Now that your user is registered, you can click the login button:

Then the user will be asked to do a second factor auth using the Fido2 device.

And you can complete the login using the hardware device.

Disable Fido2 2FA

The identity Disable Razor Page is extended to remove the Fido data for this user.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;

namespace AspNetCoreIdentityFido2Mfa.Areas.Identity.Pages.Account.Manage
{
    public class Disable2faModel : PageModel
    {
        private readonly UserManager<IdentityUser> _userManager;
        private readonly Fido2Storage _fido2Storage;
        private readonly ILogger<Disable2faModel> _logger;

        public Disable2faModel(
            UserManager<IdentityUser> userManager,
            ILogger<Disable2faModel> logger,
            Fido2Storage fido2Storage)
        {
            _userManager = userManager;
            _fido2Storage = fido2Storage;
            _logger = logger;
        }

        [TempData]
        public string StatusMessage { get; set; }

        public async Task<IActionResult> OnGet()
        {
            var user = await _userManager.GetUserAsync(User);
            if (user == null)
            {
                return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
            }

            if (!await _userManager.GetTwoFactorEnabledAsync(user))
            {
                throw new InvalidOperationException($"Cannot disable 2FA for user with ID '{_userManager.GetUserId(User)}' as it's not currently enabled.");
            }

            return Page();
        }

        public async Task<IActionResult> OnPostAsync()
        {
            var user = await _userManager.GetUserAsync(User);
            if (user == null)
            {
                return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
            }

            // remove Fido2 MFA if it exists
            await _fido2Storage.RemoveCredentialsByUsername(user.UserName);

            var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false);
            if (!disable2faResult.Succeeded)
            {
                throw new InvalidOperationException($"Unexpected error occurred disabling 2FA for user with ID '{_userManager.GetUserId(User)}'.");
            }

            _logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", _userManager.GetUserId(User));
            StatusMessage = "2fa has been disabled. You can reenable 2fa when you setup an authenticator app";
            return RedirectToPage("./TwoFactorAuthentication");
        }
    }
}

Add the recovery codes for the Fido2 2FA

The demo code is not complete, the user should be redirected to the generated recovery codes, so if he/she loses the Fido2 hardware device, the account can be recovered.

Hardware

This code and application was tested using YubiKey 5 Series. This works really good, and is only about 50$ to buy. Many online services already support this, and I would recommend this device.

Links:

https://github.com/abergs/fido2-net-lib

The YubiKey

https://www.troyhunt.com/beyond-passwords-2fa-u2f-and-google-advanced-protection/

FIDO2: WebAuthn & CTAP

https://www.w3.org/TR/webauthn/

https://www.scottbrady91.com/FIDO/A-FIDO2-Primer-and-Proof-of-Concept-using-ASPNET-Core

https://github.com/herrjemand/awesome-webauthn

https://developers.yubico.com/FIDO2/Libraries/Using_a_library.html

View at Medium.com

https://docs.microsoft.com/en-us/aspnet/core/?view=aspnetcore-3.0

https://www.nuget.org/packages/Fido2/

Securing an ASP.NET Core Razor Page App using OpenID Connect Code flow with PKCE

$
0
0

This article shows how to secure an ASP.NET Core Razor Page application using the Open ID Connect code flow with PKCE (Proof Key for Code Exchange). The secure token server is implemented using IdentityServer4 but any STS could be used which supports PKCE.

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

An ASP.NET Core 3.0 Razor Page application without identity was created using the Visual Studio templates. The Microsoft.AspNetCore.Authentication.OpenIdConnect Nuget package was then added to the project.

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="3.0.0" />
  </ItemGroup>

</Project>

In the startup class, the ConfigureServices method is used to add the authentication and the authorization. Cookies is used to persist the session, if authorized, and OpenID Connect is used to signin, signout. If a new session is started, the application redirects to IdentityServer4 and secures both the identity and the application using the OpenID Connect code flow with PKCE (Proof key for code exchange). Both the PKCE and the secret are required.

public void ConfigureServices(IServiceCollection services)
{
	services.AddAuthentication(options =>
	{
		options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
		options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
	})
   .AddCookie()
   .AddOpenIdConnect(options =>
   {
	   options.SignInScheme = "Cookies";
	   options.Authority = "https://localhost:44352";
	   options.RequireHttpsMetadata = true;
	   options.ClientId = "codeflowpkceclient";
	   options.ClientSecret = "codeflow_pkce_client_secret";
	   options.ResponseType = "code";
	   options.UsePkce = true;
	   options.Scope.Add("profile");
	   options.Scope.Add("offline_access");
	   options.SaveTokens = true;
   });

   services.AddAuthorization();
   services.AddRazorPages();
}

The Configure method adds the middleware so that the authorization is used. Both the UseAuthentication() and UseAuthorization() methods are required, and must be added after the AddRouting() method.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	// code not needed for example
	app.UseStaticFiles();

	app.UseRouting();

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

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

IdentityServer4 is configured to accept the client configuration from above. Both the PKCE and the secret are required. The configuration must match the client configuration exactly. In a production application, the secrets must be removed from the code and read from a safe configuration like for example Azure key vault. The URLs would also be read from app.settings or something like this.

new Client
{
	ClientName = "codeflowpkceclient",
	ClientId = "codeflowpkceclient",
	ClientSecrets = {new Secret("codeflow_pkce_client_secret".Sha256()) },
	AllowedGrantTypes = GrantTypes.Code,
	RequirePkce = true,
	RequireClientSecret = true,
	AllowOfflineAccess = true,
	AlwaysSendClientClaims = true,
	UpdateAccessTokenClaimsOnRefresh = true,
	AlwaysIncludeUserClaimsInIdToken = true,
	RedirectUris = {
		"https://localhost:44330/signin-oidc",
	},
	PostLogoutRedirectUris = {
		"https://localhost:44330/signout-callback-oidc",
	},
	AllowedScopes = new List<string>
	{
		IdentityServerConstants.StandardScopes.OpenId,
		IdentityServerConstants.StandardScopes.Profile,
		IdentityServerConstants.StandardScopes.OfflineAccess
	}
}

The Authorize attribute needs to be added to all pages which are to be secured. You could also require that the whole application is to be secure and opt out for the non-secure pages. If the page is called in a browser, the application will automatically redirect the user, application to authenticate.

[Authorize]
public class IndexModel : PageModel
{
	private readonly ILogger<IndexModel> _logger;

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

	public void OnGet()
	{

	}
}

The application also needs a signout. This is implemented using two new pages, a logout page, and a SignedOut page. If the user clicks the logout link, the application removes the session and redirects to a public page of the application.

[Authorize]
public class LogoutModel : PageModel
{
	public async Task<IActionResult> OnGetAsync()
	{
		await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

		return Redirect("/SignedOut");
	}
}

Now the Razor page application, identity can signin, signout using OpenID Connect Code Flow with PKCE and also uses a secret to authorize the client.

Links:

https://openid.net/specs/openid-connect-core-1_0.html

https://docs.microsoft.com/en-us/aspnet/core/security/?view=aspnetcore-3.0

https://tools.ietf.org/html/rfc7636

https://docs.microsoft.com/en-us/aspnet/core/razor-pages


Adding FIDO2 Passwordless authentication to an ASP.NET Core Identity App

$
0
0

This article shows how FIDO2 WebAuthn could be used for a passwordless sign in integrated into an ASP.NET Core Identity application. The FIDO2 WebAuthn is implemented using the fido2-net-lib Nuget package, and demo code created by Anders Åberg. The application is implemented using ASP.NET Core 3.0 with Identity. For information about FIDO2 and WebAuthn, please refer to the links at the bottom.

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

Other posts in this series

ASP.NET Core Identity with Fido2 WebAuthn MFA

Implementing the ASP.NET Core Relying Party

An ASP.NET Core Identity project was created used the Visual Studio templates. This uses EF Core with SQL Server and adds an ApplicationDbContext. The FIDO2 nuget package was added to the project as well as Microsoft.AspNetCore.Mvc.NewtonsoftJson.

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <UserSecretsId>aspnet-AspNetCoreIdentityFido2Passwordless-A24A7A38-BA5D-4D6C-A05B-54F4421C030B</UserSecretsId>
  </PropertyGroup>


  <ItemGroup>
    <PackageReference Include="Fido2" Version="1.0.1" />
    <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.0.0" />
    <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.0.0" />
    <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.0.0" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.0.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.0.0" />
    <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.0.0" />
  </ItemGroup>

</Project>

FIDO2 helper classes were created and added to the FIDO2 folder in the project. The FidoStoredCredential class is used to store the data to the database. The FIDO2 services were then added the services in the ConfigureServices method.

public void ConfigureServices(IServiceCollection services)
{
	services.AddDbContext<ApplicationDbContext>(options =>
		options.UseSqlServer(
			Configuration.GetConnectionString("DefaultConnection")));
	services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
		.AddEntityFrameworkStores<ApplicationDbContext>();

	services.AddControllers()
	  .AddNewtonsoftJson();

	services.AddRazorPages();

	services.Configure<Fido2Configuration>(Configuration.GetSection("fido2"));
	services.Configure<Fido2MdsConfiguration>(Configuration.GetSection("fido2mds"));
	services.AddScoped<Fido2Storage>();
	// Adds a default in-memory implementation of IDistributedCache.
	services.AddDistributedMemoryCache();
	services.AddSession(options =>
	{
		// Set a short timeout for easy testing.
		options.IdleTimeout = TimeSpan.FromMinutes(2);
		options.Cookie.HttpOnly = true;
		options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None;
	});
}

FIDO2 requires session and this was added as middleware in the Configure method.

public void Configure(IApplicationBuilder app)
{
	// ...
	
	app.UseStaticFiles();

	app.UseRouting();

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

	app.UseSession();

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

The settings are read from the app.settings and this needs to match your deployment, hosting.

  "fido2": {
    "serverDomain": "localhost",
    "serverName": "Fido2PasswordlessTest",
    "origin": "https://localhost:44326",
    "timestampDriftTolerance": 300000
  },
  "fido2mds": {
    "MDSAccessKey": null
  },

The ApplicationDbContext Entity Framework Core context is extended to include the FidoStoredCredential, which is used to persist the FIDO2 data. After adding this, run the migrations to create a table in the database.

using System;
using System.Collections.Generic;
using System.Text;
using Fido2Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace AspNetCoreIdentityFido2Passwordless.Data
{
    public class ApplicationDbContext : IdentityDbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        public DbSet<FidoStoredCredential> FidoStoredCredential { get; set; }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            builder.Entity<FidoStoredCredential>().HasKey(m => m.Username);

            base.OnModelCreating(builder);
        }
    }
}

Passwordless Register and Authentication with Identity

The register and the sign in controllers are uses to execute the FIDO2 password flow. The URLs used must match the URLS set in the WebAuthn javascript implementation. The ASP.NET Core Identity UserManager is used to create an Identity, if the FIDO2 register completes successfully.

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Fido2NetLib.Objects;
using Fido2NetLib;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using static Fido2NetLib.Fido2;
using System.IO;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;

namespace Fido2Identity
{

    [Route("api/[controller]")]
    public class PwFido2RegisterController : Controller
    {
        private Fido2 _lib;
        public static IMetadataService _mds;
        private readonly Fido2Storage _fido2Storage;
        private readonly UserManager<IdentityUser> _userManager;
        private readonly IOptions<Fido2Configuration> _optionsFido2Configuration;
        private readonly IOptions<Fido2MdsConfiguration> _optionsFido2MdsConfiguration;
        

        public PwFido2RegisterController(
            Fido2Storage fido2Storage, 
            UserManager<IdentityUser> userManager,
            IOptions<Fido2Configuration> optionsFido2Configuration,
            IOptions<Fido2MdsConfiguration> optionsFido2MdsConfiguration)
        {
            _userManager = userManager;
            _optionsFido2Configuration = optionsFido2Configuration;
            _optionsFido2MdsConfiguration = optionsFido2MdsConfiguration;
            _fido2Storage = fido2Storage;

            var MDSCacheDirPath = _optionsFido2MdsConfiguration.Value.MDSCacheDirPath ?? Path.Combine(Path.GetTempPath(), "fido2mdscache"); 
            _mds = string.IsNullOrEmpty(_optionsFido2MdsConfiguration.Value.MDSAccessKey) ? null : MDSMetadata.Instance(
                _optionsFido2MdsConfiguration.Value.MDSAccessKey, MDSCacheDirPath);
            if (null != _mds)
            {
                if (false == _mds.IsInitialized())
                    _mds.Initialize().Wait();
            }

            _lib = new Fido2(new Fido2Configuration()
            {
                ServerDomain = _optionsFido2Configuration.Value.ServerDomain,
                ServerName = _optionsFido2Configuration.Value.ServerName,
                Origin = _optionsFido2Configuration.Value.Origin,
                // Only create and use Metadataservice if we have an acesskey
                MetadataService = _mds,
                TimestampDriftTolerance = _optionsFido2Configuration.Value.TimestampDriftTolerance
            });
        }

        private string FormatException(Exception e)
        {
            return string.Format("{0}{1}", e.Message, e.InnerException != null ? " (" + e.InnerException.Message + ")" : "");
        }

        [HttpPost]
        [Route("/pwmakeCredentialOptions")]
        public async Task<JsonResult> MakeCredentialOptions([FromForm] string username, [FromForm] string displayName, [FromForm] string attType, [FromForm] string authType, [FromForm] bool requireResidentKey, [FromForm] string userVerification)
        {
            try
            {
                if (string.IsNullOrEmpty(username))
                {
                    username = $"{displayName} (Usernameless user created at {DateTime.UtcNow})";
                }

                var user = new Fido2User
                {
                    DisplayName = displayName,
                    Name = username,
                    Id = Encoding.UTF8.GetBytes(username) // byte representation of userID is required
                };

                // 2. Get user existing keys by username
                var items = await _fido2Storage.GetCredentialsByUsername(username);
                var existingKeys = new List<PublicKeyCredentialDescriptor>();
                foreach(var publicKeyCredentialDescriptor in items)
                {
                    existingKeys.Add(publicKeyCredentialDescriptor.Descriptor);
                }

                // 3. Create options
                var authenticatorSelection = new AuthenticatorSelection
                {
                    RequireResidentKey = requireResidentKey,
                    UserVerification = userVerification.ToEnum<UserVerificationRequirement>()
                };

                if (!string.IsNullOrEmpty(authType))
                    authenticatorSelection.AuthenticatorAttachment = authType.ToEnum<AuthenticatorAttachment>();

                var exts = new AuthenticationExtensionsClientInputs() { Extensions = true, UserVerificationIndex = true, Location = true, UserVerificationMethod = true, BiometricAuthenticatorPerformanceBounds = new AuthenticatorBiometricPerfBounds { FAR = float.MaxValue, FRR = float.MaxValue } };

                var options = _lib.RequestNewCredential(user, existingKeys, authenticatorSelection, attType.ToEnum<AttestationConveyancePreference>(), exts);

                // 4. Temporarily store options, session/in-memory cache/redis/db
                HttpContext.Session.SetString("fido2.attestationOptions", options.ToJson());

                // 5. return options to client
                return Json(options);
            }
            catch (Exception e)
            {
                return Json(new CredentialCreateOptions { Status = "error", ErrorMessage = FormatException(e) });
            }
        }

        [HttpPost]
        [Route("/pwmakeCredential")]
        public async Task<JsonResult> MakeCredential([FromBody] AuthenticatorAttestationRawResponse attestationResponse)
        {
            try
            {
                // 1. get the options we sent the client
                var jsonOptions = HttpContext.Session.GetString("fido2.attestationOptions");
                var options = CredentialCreateOptions.FromJson(jsonOptions);

                // 2. Create callback so that lib can verify credential id is unique to this user
                IsCredentialIdUniqueToUserAsyncDelegate callback = async (IsCredentialIdUniqueToUserParams args) =>
                {
                    var users = await _fido2Storage.GetUsersByCredentialIdAsync(args.CredentialId);
                    if (users.Count > 0) return false;

                    return true;
                };

                // 2. Verify and make the credentials
                var success = await _lib.MakeNewCredentialAsync(attestationResponse, options, callback);

                // 3. Store the credentials in db
                await _fido2Storage.AddCredentialToUser(options.User, new FidoStoredCredential
                {
                    Username = options.User.Name,
                    Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId),
                    PublicKey = success.Result.PublicKey,
                    UserHandle = success.Result.User.Id,
                    SignatureCounter = success.Result.Counter,
                    CredType = success.Result.CredType,
                    RegDate = DateTime.Now,
                    AaGuid = success.Result.Aaguid
                });

                // 4. return "ok" to the client

                var user = await CreateUser(options.User.Name);
                // await _userManager.GetUserAsync(User);

                if (user == null)
                {
                    return Json(new CredentialMakeResult { Status = "error", ErrorMessage = $"Unable to load user with ID '{_userManager.GetUserId(User)}'." });
                }

                return Json(success);
            }
            catch (Exception e)
            {
                return Json(new CredentialMakeResult { Status = "error", ErrorMessage = FormatException(e) });
            }
        }

        private async Task<IdentityUser> CreateUser(string userEmail)
        {
            var user = new IdentityUser { UserName = userEmail, Email = userEmail, EmailConfirmed = true };
            var result = await _userManager.CreateAsync(user);
            if (result.Succeeded)
            {
                //await _signInManager.SignInAsync(user, isPersistent: false);
            }

            return user;
        }
    }
}

The PwFido2SignInController implements the FIDO2 passwordless sign in. This uses ASP.NET Core Identity to sign in the user, if the FIDO2 flow completes successfully.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Fido2NetLib.Objects;
using Fido2NetLib;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.IO;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;

namespace Fido2Identity
{

    [Route("api/[controller]")]
    public class PwFido2SignInController : Controller
    {
        private Fido2 _lib;
        public static IMetadataService _mds;
        private readonly Fido2Storage _fido2Storage;
        private readonly UserManager<IdentityUser> _userManager;
        private readonly SignInManager<IdentityUser> _signInManager;
        private readonly IOptions<Fido2Configuration> _optionsFido2Configuration;
        private readonly IOptions<Fido2MdsConfiguration> _optionsFido2MdsConfiguration;

        public PwFido2SignInController(
            Fido2Storage fido2Storage,
            UserManager<IdentityUser> userManager,
            SignInManager<IdentityUser> signInManager,
            IOptions<Fido2Configuration> optionsFido2Configuration,
            IOptions<Fido2MdsConfiguration> optionsFido2MdsConfiguration)
        {
            _userManager = userManager;
            _optionsFido2Configuration = optionsFido2Configuration;
            _optionsFido2MdsConfiguration = optionsFido2MdsConfiguration;
            _signInManager = signInManager;
            _userManager = userManager;
            _fido2Storage = fido2Storage;

            var MDSCacheDirPath = _optionsFido2MdsConfiguration.Value.MDSCacheDirPath ?? Path.Combine(Path.GetTempPath(), "fido2mdscache");
            _mds = string.IsNullOrEmpty(_optionsFido2MdsConfiguration.Value.MDSAccessKey) ? null : MDSMetadata.Instance(
                _optionsFido2MdsConfiguration.Value.MDSAccessKey, MDSCacheDirPath); 
            
            if (null != _mds)
            {
                if (false == _mds.IsInitialized())
                    _mds.Initialize().Wait();
            }

            _lib = new Fido2(new Fido2Configuration()
            {
                ServerDomain = _optionsFido2Configuration.Value.ServerDomain,
                ServerName = _optionsFido2Configuration.Value.ServerName,
                Origin = _optionsFido2Configuration.Value.Origin,
                // Only create and use Metadataservice if we have an acesskey
                MetadataService = _mds,
                TimestampDriftTolerance = _optionsFido2Configuration.Value.TimestampDriftTolerance
            });
        }

        private string FormatException(Exception e)
        {
            return string.Format("{0}{1}", e.Message, e.InnerException != null ? " (" + e.InnerException.Message + ")" : "");
        }

        [HttpPost]
        [Route("/pwassertionOptions")]
        public async Task<ActionResult> AssertionOptionsPost([FromForm] string username, [FromForm] string userVerification)
        {
            try
            {

                var existingCredentials = new List<PublicKeyCredentialDescriptor>();

                if (!string.IsNullOrEmpty(username))
                {
                    var identityUser = await _userManager.FindByNameAsync(username);
                    var user = new Fido2User
                    {
                        DisplayName = identityUser.UserName,
                        Name = identityUser.UserName,
                        Id = Encoding.UTF8.GetBytes(identityUser.UserName) // byte representation of userID is required
                    };

                    if (user == null) throw new ArgumentException("Username was not registered");

                    // 2. Get registered credentials from database
                    var items = await _fido2Storage.GetCredentialsByUsername(identityUser.UserName);
                    existingCredentials = items.Select(c => c.Descriptor).ToList();
                }

                var exts = new AuthenticationExtensionsClientInputs() { SimpleTransactionAuthorization = "FIDO", GenericTransactionAuthorization = new TxAuthGenericArg { ContentType = "text/plain", Content = new byte[] { 0x46, 0x49, 0x44, 0x4F } }, UserVerificationIndex = true, Location = true, UserVerificationMethod = true };

                // 3. Create options
                var uv = string.IsNullOrEmpty(userVerification) ? UserVerificationRequirement.Discouraged : userVerification.ToEnum<UserVerificationRequirement>();
                var options = _lib.GetAssertionOptions(
                    existingCredentials,
                    uv,
                    exts
                );

                // 4. Temporarily store options, session/in-memory cache/redis/db
                HttpContext.Session.SetString("fido2.assertionOptions", options.ToJson());

                // 5. Return options to client
                return Json(options);
            }

            catch (Exception e)
            {
                return Json(new AssertionOptions { Status = "error", ErrorMessage = FormatException(e) });
            }
        }

        [HttpPost]
        [Route("/pwmakeAssertion")]
        public async Task<JsonResult> MakeAssertion([FromBody] AuthenticatorAssertionRawResponse clientResponse)
        {
            try
            {
                // 1. Get the assertion options we sent the client
                var jsonOptions = HttpContext.Session.GetString("fido2.assertionOptions");
                var options = AssertionOptions.FromJson(jsonOptions);

                // 2. Get registered credential from database
                var creds = await _fido2Storage.GetCredentialById(clientResponse.Id);

                if (creds == null)
                {
                    throw new Exception("Unknown credentials");
                }

                // 3. Get credential counter from database
                var storedCounter = creds.SignatureCounter;

                // 4. Create callback to check if userhandle owns the credentialId
                IsUserHandleOwnerOfCredentialIdAsync callback = async (args) =>
                {
                    var storedCreds = await _fido2Storage.GetCredentialsByUserHandleAsync(args.UserHandle);
                    return storedCreds.Exists(c => c.Descriptor.Id.SequenceEqual(args.CredentialId));
                };

                // 5. Make the assertion
                var res = await _lib.MakeAssertionAsync(clientResponse, options, creds.PublicKey, storedCounter, callback);

                // 6. Store the updated counter
                await _fido2Storage.UpdateCounter(res.CredentialId, res.Counter);

                var identityUser = await _userManager.FindByNameAsync(creds.Username);
                if (identityUser == null)
                {
                    throw new InvalidOperationException($"Unable to load user.");
                }
                
                await _signInManager.SignInAsync(identityUser, isPersistent: false);

                // 7. return OK to client
                return Json(res);
            }
            catch (Exception e)
            {
                return Json(new AssertionVerificationResult { Status = "error", ErrorMessage = FormatException(e) });
            }
        }
    }
}

Implementing the WebAuthn javascript APIs

The WebAuthn FIDO2 passwordless flow is implemented in javascript. We need to replace the Identity login, and register pages with the FIDO2 logic. To do this, the Login and the Register Identity pages are scaffolded into the project using Visual Studio.

The Logic from the Register.cshtml.cs is completely removed, and replaced with the following code. We do not want to register using a password.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace AspNetCoreIdentityFido2Passwordless.Areas.Identity.Pages.Account
{
    [AllowAnonymous]
    public class RegisterModel : PageModel
    {
        public void OnGet()
        {
        }

        public void OnPost()
        {
        }
    }
}

The HTML part of the Register page is replaced and the passwordless.register.js WebAuthn implementation is added here.

@page
@{
    ViewData["Title"] = "Register";
}

<h1>@ViewData["Title"]</h1>

<div class="row">
    <div class="col-md-4">
        <form action="/mfa" method="post" id="register">
            <div class="form-group">
                <label name="username">Email</label>
                <input name="username" class="form-control" />
            </div>

            <div class="form-group">
                <label name="displayName">Display name</label>
                <input name="displayName" class="form-control" />
            </div>

            <div class="field">
                <div class="control">
                    <button class="btn btn-primary">Register user</button>
                </div>
            </div>
        </form>
    </div>
</div>

<script src="~/js/helpers.js"></script>
<script src="~/js/instant.js"></script>
<script src="~/js/passwordless.register.js"></script>

The Login.cshtml.cs logic is also completely removed and replaced with the following:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace AspNetCoreIdentityFido2Passwordless.Areas.Identity.Pages.Account
{
    [AllowAnonymous]
    public class LoginModel : PageModel
    {
        public void OnGet()
        {
        }

        public void OnPost()
        {
        }
    }
}

The HTML part of the page implements the form which uses the passwordless.login.js jaavascript functions.

@page
@model LoginModel

@{
    ViewData["Title"] = "Log in";
}

<h1>@ViewData["Title"]</h1>
<div class="row">
    <div class="col-md-4">
        <section>
            <form action="/mfa" method="post" id="signin">
                <div class="form-group">
                    <label name="username">Email</label>
                    <input name="username" class="form-control" />
                </div>

                <div class="field">
                    <div class="control">
                        <button class="btn btn-primary">Login</button>
                    </div>
                </div>
            </form>
        </section>
    </div>

</div>

<script src="~/js/helpers.js"></script>
<script src="~/js/instant.js"></script>
<script src="~/js/passwordless.login.js"></script>

The passwordless.login.js and the passwordless.register.js have require sweetalert2 and other javascript packages. These are added in the _Layout view. You could remove these if you want, and update the 2 javacript files, not to use these.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - AspNetCoreIdentityFido2Passwordless</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" />

    <link href="https://fonts.googleapis.com/css?family=Work+Sans" rel="stylesheet">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js" integrity="sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4" crossorigin="anonymous"></script>
    <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/sweetalert2"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/limonte-sweetalert2/6.10.1/sweetalert2.min.css" />
    <script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
</head>

The register WebAuthn is implemented in the passwordless.register.js. This is more or less the code from the fido2-net-lib demo project, except the URLs have been changed. In a production app, this would need to be cleaned up.

document.getElementById('register').addEventListener('submit', handleRegisterSubmit);

async function handleRegisterSubmit(event) {
    event.preventDefault();

    let username = this.username.value;
    let displayName = this.displayName.value;

    // possible values: none, direct, indirect
    let attestation_type = "none";
    // possible values: <empty>, platform, cross-platform
    let authenticator_attachment = "";

    // possible values: preferred, required, discouraged
    let user_verification = "preferred";

    // possible values: true,false
    let require_resident_key = false;

    // prepare form post data
    var data = new FormData();
    data.append('username', username);
    data.append('displayName', displayName);
    data.append('attType', attestation_type);
    data.append('authType', authenticator_attachment);
    data.append('userVerification', user_verification);
    data.append('requireResidentKey', require_resident_key);

    // send to server for registering
    let makeCredentialOptions;
    try {
        makeCredentialOptions = await fetchMakeCredentialOptions(data);

    } catch (e) {
        console.error(e);
        let msg = "Something wen't really wrong";
        showErrorAlert(msg);
    }


    console.log("Credential Options Object", makeCredentialOptions);

    if (makeCredentialOptions.status !== "ok") {
        console.log("Error creating credential options");
        console.log(makeCredentialOptions.errorMessage);
        showErrorAlert(makeCredentialOptions.errorMessage);
        return;
    }

    // Turn the challenge back into the accepted format of padded base64
    makeCredentialOptions.challenge = coerceToArrayBuffer(makeCredentialOptions.challenge);
    // Turn ID into a UInt8Array Buffer for some reason
    makeCredentialOptions.user.id = coerceToArrayBuffer(makeCredentialOptions.user.id);

    makeCredentialOptions.excludeCredentials = makeCredentialOptions.excludeCredentials.map((c) => {
        c.id = coerceToArrayBuffer(c.id);
        return c;
    });

    if (makeCredentialOptions.authenticatorSelection.authenticatorAttachment === null) makeCredentialOptions.authenticatorSelection.authenticatorAttachment = undefined;

    console.log("Credential Options Formatted", makeCredentialOptions);

    Swal.fire({
        title: 'Registering...',
        text: 'Tap your security key to finish registration.',
        imageUrl: "/images/securitykey.min.svg",
        showCancelButton: true,
        showConfirmButton: false,
        focusConfirm: false,
        focusCancel: false
    });


    console.log("Creating PublicKeyCredential...");

    let newCredential;
    try {
        newCredential = await navigator.credentials.create({
            publicKey: makeCredentialOptions
        });
    } catch (e) {
        var msg = "Could not create credentials in browser. Probably because the username is already registered with your authenticator. Please change username or authenticator."
        console.error(msg, e);
        showErrorAlert(msg, e);
    }


    console.log("PublicKeyCredential Created", newCredential);

    try {
        registerNewCredential(newCredential);

    } catch (e) {
        showErrorAlert(err.message ? err.message : err);
    }
}

async function fetchMakeCredentialOptions(formData) {
    let response = await fetch('/pwmakeCredentialOptions', {
        method: 'POST', // or 'PUT'
        body: formData, // data can be `string` or {object}!
        headers: {
            'Accept': 'application/json'
        }
    });

    let data = await response.json();

    return data;
}


// This should be used to verify the auth data with the server
async function registerNewCredential(newCredential) {
    // Move data into Arrays incase it is super long
    let attestationObject = new Uint8Array(newCredential.response.attestationObject);
    let clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON);
    let rawId = new Uint8Array(newCredential.rawId);

    const data = {
        id: newCredential.id,
        rawId: coerceToBase64Url(rawId),
        type: newCredential.type,
        extensions: newCredential.getClientExtensionResults(),
        response: {
            AttestationObject: coerceToBase64Url(attestationObject),
            clientDataJson: coerceToBase64Url(clientDataJSON)
        }
    };

    let response;
    try {
        response = await registerCredentialWithServer(data);
    } catch (e) {
        showErrorAlert(e);
    }

    console.log("Credential Object", response);

    // show error
    if (response.status !== "ok") {
        console.log("Error creating credential");
        console.log(response.errorMessage);
        showErrorAlert(response.errorMessage);
        return;
    }

    // show success 
    Swal.fire({
        title: 'Registration Successful!',
        text: 'You\'ve registered successfully.',
        type: 'success',
        timer: 2000
    });

    // redirect to dashboard?
    //window.location.href = "/dashboard/" + state.user.displayName;
}

async function registerCredentialWithServer(formData) {
    let response = await fetch('/pwmakeCredential', {
        method: 'POST', // or 'PUT'
        body: JSON.stringify(formData), // data can be `string` or {object}!
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        }
    });

    let data = await response.json();

    return data;
}

The passwordless login WebAuthn is implemented in the passwordless.login.js. This is more or less the code from the fido2-net-lib demo project, except the URLs have been changed.

document.getElementById('signin').addEventListener('submit', handleSignInSubmit);

async function handleSignInSubmit(event) {
    event.preventDefault();

    let username = this.username.value;

    // prepare form post data
    var formData = new FormData();
    formData.append('username', username);

    // send to server for registering
    let makeAssertionOptions;
    try {
        var res = await fetch('/pwassertionOptions', {
            method: 'POST', // or 'PUT'
            body: formData, // data can be `string` or {object}!
            headers: {
                'Accept': 'application/json'
            }
        });

        makeAssertionOptions = await res.json();
    } catch (e) {
        showErrorAlert("Request to server failed", e);
    }

    console.log("Assertion Options Object", makeAssertionOptions);

    // show options error to user
    if (makeAssertionOptions.status !== "ok") {
        console.log("Error creating assertion options");
        console.log(makeAssertionOptions.errorMessage);
        showErrorAlert(makeAssertionOptions.errorMessage);
        return;
    }

    // todo: switch this to coercebase64
    const challenge = makeAssertionOptions.challenge.replace(/-/g, "+").replace(/_/g, "/");
    makeAssertionOptions.challenge = Uint8Array.from(atob(challenge), c => c.charCodeAt(0));

    // fix escaping. Change this to coerce
    makeAssertionOptions.allowCredentials.forEach(function (listItem) {
        var fixedId = listItem.id.replace(/\_/g, "/").replace(/\-/g, "+");
        listItem.id = Uint8Array.from(atob(fixedId), c => c.charCodeAt(0));
    });

    console.log("Assertion options", makeAssertionOptions);

    Swal.fire({
        title: 'Logging In...',
        text: 'Tap your security key to login.',
        imageUrl: "/images/securitykey.min.svg",
        showCancelButton: true,
        showConfirmButton: false,
        focusConfirm: false,
        focusCancel: false
    });

    // ask browser for credentials (browser will ask connected authenticators)
    let credential;
    try {
        credential = await navigator.credentials.get({ publicKey: makeAssertionOptions })
    } catch (err) {
        showErrorAlert(err.message ? err.message : err);
    }

    try {
        await verifyAssertionWithServer(credential);
    } catch (e) {
        showErrorAlert("Could not verify assertion", e);
    }
}

async function verifyAssertionWithServer(assertedCredential) {

    // Move data into Arrays incase it is super long
    let authData = new Uint8Array(assertedCredential.response.authenticatorData);
    let clientDataJSON = new Uint8Array(assertedCredential.response.clientDataJSON);
    let rawId = new Uint8Array(assertedCredential.rawId);
    let sig = new Uint8Array(assertedCredential.response.signature);
    const data = {
        id: assertedCredential.id,
        rawId: coerceToBase64Url(rawId),
        type: assertedCredential.type,
        extensions: assertedCredential.getClientExtensionResults(),
        response: {
            authenticatorData: coerceToBase64Url(authData),
            clientDataJson: coerceToBase64Url(clientDataJSON),
            signature: coerceToBase64Url(sig)
        }
    };

    let response;
    try {
        let res = await fetch("/pwmakeAssertion", {
            method: 'POST', // or 'PUT'
            body: JSON.stringify(data), // data can be `string` or {object}!
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json'
            }
        });

        response = await res.json();
    } catch (e) {
        showErrorAlert("Request to server failed", e);
        throw e;
    }

    console.log("Assertion Object", response);

    // show error
    if (response.status !== "ok") {
        console.log("Error doing assertion");
        console.log(response.errorMessage);
        showErrorAlert(response.errorMessage);
        return;
    }

    // show success message
    await Swal.fire({
        title: 'Logged In!',
        text: 'You\'re logged in successfully.',
        type: 'success',
        timer: 2000
    });

    window.location.href = "/index";
}

Now when the application is started, you can register and authenticate using a FIDO2 passwordless flow with ASP.NET Core Identity. If you do use the password flow, you should consider forcing a second factor on the FIDO2 device like using a pin or a biometric validation, so that if the device is lost, a second factor is still required to use the authenticator with the webpage.

Links:

https://github.com/abergs/fido2-net-lib

https://webauthn.io/

https://webauthn.guide

The YubiKey

https://www.troyhunt.com/beyond-passwords-2fa-u2f-and-google-advanced-protection/

FIDO2: WebAuthn & CTAP

https://www.w3.org/TR/webauthn/

https://www.scottbrady91.com/FIDO/A-FIDO2-Primer-and-Proof-of-Concept-using-ASPNET-Core

https://github.com/herrjemand/awesome-webauthn

https://developers.yubico.com/FIDO2/Libraries/Using_a_library.html

View at Medium.com

https://docs.microsoft.com/en-us/aspnet/core/?view=aspnetcore-3.0

https://www.nuget.org/packages/Fido2/

Securing a Web API using multiple token servers

$
0
0

This article shows how a single secure Web API could be used together with multiple secure token servers. The API uses JWT Bearer token authentication, but because the access token come from different token servers, the tokens validation need to be changed.

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

Using multiple Authorities with shared certitficate

The first way this can be supported, is that the Authority option is removed from the AddJwtBearer code configuration. When this is removed, the JWT Bearer has no way of validating the Issuer signing. The same certificate which signed the access token, needs to be used in the API as well as the token server. This is easy if you have control of all the applications and can read the certificate from a shared resource like Azure Key Vault. Multiple Issuers can then be supported by configuring the TokenValidationParameters where the signing key is also coded.

public void ConfigureServices(IServiceCollection services)
{
	var x509Certificate2 = GetCertificate(_environment);

	services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
	   .AddJwtBearer(options =>
		{
			options.Audience = "ProtectedApiResource";
			options.TokenValidationParameters = new TokenValidationParameters
			{
				ValidateIssuer = true,
				ValidIssuers = new List<string> { "https://localhost:44318", "https://localhost:44367" },
				ValidateIssuerSigningKey = true,
				IssuerSigningKey = new X509SecurityKey(x509Certificate2),
				IssuerSigningKeyResolver =
				(string token, SecurityToken securityToken, string kid, TokenValidationParameters validationParameters)
				  => new List<X509SecurityKey> { new X509SecurityKey(x509Certificate2) }
			};
		});

	services.AddAuthorization(options =>
		options.AddPolicy("protectedScope", policy =>
		{
			policy.RequireClaim("scope", "scope_used_for_api_in_protected_zone");
		})
	);

	services.AddControllers();
}

Using multiple Schemes

A different solution would be to use multiple Authentication schemes, a different one for each token service. The Schemes are then added to the default authorization policy. The example shown underneath requires a scope claim scope_used_for_api_in_protected_zone.

public void ConfigureServices(IServiceCollection services)
{
	var x509Certificate2 = GetCertificate(_environment);

	services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
		.AddJwtBearer("JWT", options =>
		{
			options.Audience = "ProtectedApiResourceOne";
			options.Authority = "https://localhost:44318";
		})
		.AddJwtBearer("Custom", options =>
		{
			options.Audience = "ProtectedApiResourceTwo";
			options.Authority = "https://localhost:44367";
		});

	services.AddAuthorization(options =>
	{
		options.DefaultPolicy = new AuthorizationPolicyBuilder()
			.RequireAuthenticatedUser()
			.AddAuthenticationSchemes("JWT", "Custom")
			.Build();

		options.AddPolicy("protectedScope", policy =>
		{
			// scope is required in token from both servers
			policy.RequireClaim("scope", "scope_used_for_api_in_protected_zone");
		});
	  });

	services.AddControllers();
}

Deploy two seperate Web APIs

My favourite solution would be to deploy 2 separate APIs each using a different token service. This would require two separate deployments.

All solutions have advantages, and disadvantages.

Links:

https://docs.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme

https://docs.microsoft.com/en-us/dotnet/framework/security/json-web-token-handler

User claims in ASP.NET Core using OpenID Connect Authentication

$
0
0

This article shows two possible ways of getting user claims in an ASP.NET Core application which uses an IdentityServer4 service. Both ways have advantages and require setting different code configurations in both applications.

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

To use OpenID Connect in an ASP.NET Core application, the Microsoft.AspNetCore.Authentication.OpenIdConnect package can be used. This needs to be added as a reference in the project.

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="3.0.0" />
  </ItemGroup>

</Project>

Option 1: Returning the claims in the id_token

The profile claims can be returned in the id_token which is returned after a successful authentication. The ASP.NET Core client application just needs to request the profile scope.

public void ConfigureServices(IServiceCollection services)
{
	services.AddAuthentication(options =>
	{
		options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
		options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
	})
   .AddCookie()
   .AddOpenIdConnect(options =>
   {
	   options.SignInScheme = "Cookies";
	   options.Authority = "https://localhost:44352";
	   options.RequireHttpsMetadata = true;
	   options.ClientId = "codeflowpkceclient";
	   options.ClientSecret = "codeflow_pkce_client_secret";
	   options.ResponseType = "code";
	   options.UsePkce = true;
	   options.Scope.Add("profile");
	   options.Scope.Add("offline_access");
	   options.SaveTokens = true;
   });

   services.AddAuthorization();
   services.AddRazorPages();
}

In IdentityServer4, the corresponding client configuration uses the AlwaysIncludeUserClaimsInIdToken property to include the user profile claims in the id_token. By implementing the IProfileService, any claims can be added.

With this, all claims will be returned in the id_token and can then be used in the client application. This increases the size of the token, which might be important if you add to many claims. All values will be included and available in the User.Identity context in the client application.

new Client
{
	ClientName = "codeflowpkceclient",
	ClientId = "codeflowpkceclient",
	ClientSecrets = {new Secret("codeflow_pkce_client_secret".Sha256()) },
	AllowedGrantTypes = GrantTypes.Code,
	RequirePkce = true,
	RequireClientSecret = true,
	AllowOfflineAccess = true,
	AlwaysSendClientClaims = true,
	UpdateAccessTokenClaimsOnRefresh = true,
	AlwaysIncludeUserClaimsInIdToken = true,
	RedirectUris = {
		"https://localhost:44330/signin-oidc",
		$"{codeFlowClientUrl}/signin-oidc"
	},
	PostLogoutRedirectUris = {
		"https://localhost:44330/signout-callback-oidc",
		$"{codeFlowClientUrl}/signout-callback-oidc"
	},
	AllowedScopes = new List<string>
	{
		IdentityServerConstants.StandardScopes.OpenId,
		IdentityServerConstants.StandardScopes.Profile,
		IdentityServerConstants.StandardScopes.OfflineAccess,
		"role"
	}
}

Option 2: Returning the claims using the UserInfo API

A second way to get the user claims is to use the OpenID Connect User Info API. The ASP.NET Core client application uses the GetClaimsFromUserInfoEndpoint property to configure this. One important difference to option 1, is that you MUST specify the claims you require using the MapUniqueJsonKey method, otherwise only the name, given_name and email standard claims will be available in the client application. The claims included in the id_token are mapped per default. This is the major difference to the first option. You must explicit define some of the standard claims you require.

public void ConfigureServices(IServiceCollection services)
{
	services.AddAuthentication(options =>
	{
		options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
		options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
	})
   .AddCookie()
   .AddOpenIdConnect(options =>
   {
	   options.SignInScheme = "Cookies";
	   options.Authority = "https://localhost:44352";
	   options.RequireHttpsMetadata = true;
	   options.ClientId = "codeflowpkceclient";
	   options.ClientSecret = "codeflow_pkce_client_secret";
	   options.ResponseType = "code";
	   options.UsePkce = true;
	   options.Scope.Add("profile");
	   options.Scope.Add("offline_access");
	   options.SaveTokens = true;
	   options.GetClaimsFromUserInfoEndpoint = true;
	   options.ClaimActions.MapUniqueJsonKey("preferred_username", "preferred_username");
	   options.ClaimActions.MapUniqueJsonKey("gender", "gender");
   });

   services.AddAuthorization();
   services.AddRazorPages();
}

The IdentityServer4 can be configured without the AlwaysIncludeUserClaimsInIdToken set.

new Client
{
	ClientName = "codeflowpkceclient",
	ClientId = "codeflowpkceclient",
	ClientSecrets = {new Secret("codeflow_pkce_client_secret".Sha256()) },
	AllowedGrantTypes = GrantTypes.Code,
	RequirePkce = true,
	RequireClientSecret = true,
	AllowOfflineAccess = true,
	AlwaysSendClientClaims = true,
	UpdateAccessTokenClaimsOnRefresh = true,
	//AlwaysIncludeUserClaimsInIdToken = true,
	RedirectUris = {
		"https://localhost:44330/signin-oidc",
		$"{codeFlowClientUrl}/signin-oidc"
	},
	PostLogoutRedirectUris = {
		"https://localhost:44330/signout-callback-oidc",
		$"{codeFlowClientUrl}/signout-callback-oidc"
	},
	AllowedScopes = new List<string>
	{
		IdentityServerConstants.StandardScopes.OpenId,
		IdentityServerConstants.StandardScopes.Profile,
		IdentityServerConstants.StandardScopes.OfflineAccess,
		"role"
	}
}

Mapping the Name property for the http user context.

The User.Identity.Name property can be matched from any claim using the TokenValidationParameters. If the default value is not returned, then you need to map this explicitly.

options.TokenValidationParameters = new TokenValidationParameters
{
  NameClaimType = "email", 
  // RoleClaimType = "role"
};

ASP.NET Core also does some magic mapping as a default. Some claims are removed, and some are added. If you want to take control of this, you can turn this off as follows:

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

Links:

https://openid.net/specs/openid-connect-core-1_0.html

https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims

https://docs.microsoft.com/en-us/aspnet/core/security/?view=aspnetcore-3.0

https://tools.ietf.org/html/rfc7636

https://docs.microsoft.com/en-us/aspnet/core/razor-pages

Missing Claims in the ASP.NET Core 2 OpenID Connect Handler?

Using HTTP Request Routes, Request Body, and Query string parameters for Authorization in ASP.NET Core

$
0
0

This post shows how HTTP route parameters, a HTTP request body or HTTP request query string parameters can be used for authorization in ASP.NET Core.

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

Authorization using ASP.NET Core Route parameters

An AuthorizationHandler can be used to implement authorization logic in ASP.NET Core. The handler can authorize HTTP requests using a route parameter from where the policy for the requirement used in the handler is defined. The IHttpContextAccessor is used to access the route parameters. The RouteValues property in the request of the HttpContext contains these values. If you know the name of the route value, the value can be retrieved using this key. In this demo, a static text is used to validate the route parameter value. In a real world AuthorizationHandler, the value would be validated against a claim from the token, or queried from a database, or an authorization service. To validate this correctly, something must be used which cannot be manipulated. If using a claim from the access token, then the access token must be validated fully and correctly. The AuthorizationHandler implements the ValuesRouteRequirement which is used in the policy definition.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;

namespace AppAuthorizationService
{
    public class ValuesCheckRouteParameterHandler : AuthorizationHandler<ValuesRouteRequirement>
    {
        private readonly IHttpContextAccessor _httpContextAccessor;

        public ValuesCheckRouteParameterHandler(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
        }

        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ValuesRouteRequirement requirement)
        {
            var routeValues = _httpContextAccessor.HttpContext.Request.RouteValues;

            object user;
            routeValues.TryGetValue("user", out user);
            if ( user.ToString() == "phil")
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

In the Startup class using the ConfigureServices method, the IAuthorizationHandler services are registered and also the IHttpContextAccessor using the AddHttpContextAccessor method. The policies are defined for the authorization requirements. The demo is an API project example, which uses swagger so the AddControllers extension method is used, with AddNewtonsoftJson.

public void ConfigureServices(IServiceCollection services)
{
	// ...

	services.AddHttpContextAccessor();

	services.AddSingleton<IAuthorizationHandler, ValuesCheckQueryParameterHandler>();
	services.AddSingleton<IAuthorizationHandler, ValuesCheckRequestBodyHandler>();
	services.AddSingleton<IAuthorizationHandler, ValuesCheckRouteParameterHandler>();

	services.AddAuthorization(options =>
	{
		options.AddPolicy("protectedScope", policy =>
		{
			policy.RequireClaim("scope", "native_api");
		});
		options.AddPolicy("ValuesRoutePolicy", valuesRoutePolicy =>
		{
			valuesRoutePolicy.Requirements.Add(new ValuesRouteRequirement());
		});
		options.AddPolicy("ValuesQueryPolicy", valuesQueryPolicy =>
		{
			valuesQueryPolicy.Requirements.Add(new ValuesCheckQueryParamRequirement());
		});
		options.AddPolicy("ValuesRequestBodyCheckPolicy", valuesRequestBodyCheckPolicy =>
		{
			valuesRequestBodyCheckPolicy.Requirements.Add(new ValuesRequestBodyRequirement());
		});
	});


	services.AddControllers()
		.AddNewtonsoftJson();
}

The policy is then used in the controller in the authorize attribute. In this demo, if the user has the value ‘phil’, the data will be returned, otherwise a 403 is returned, or 401 if no bearer access token is sent in the header of the HTTP request.

[Authorize("ValuesRoutePolicy")]
[ProducesResponseType(StatusCodes.Status200OK)]
[HttpGet]
[Route("{user}", Name = nameof(GetWithRouteParam))]
public IActionResult GetWithRouteParam([FromRoute]string user)
{
	return Ok($"get this data [{user}] using the route");
}

A HttpClient implementation can then make a HTTP request with the route set and the access token added to the headers.

private static async Task CallApiwithRouteValue(string currentAccessToken, string user)
{
	_apiClient.SetBearerToken(currentAccessToken);
	var response = await _apiClient.GetAsync($"/api/values/{user}");

	if (response.IsSuccessStatusCode)
	{
		var result = await response.Content.ReadAsStringAsync();
		Console.WriteLine($"\n{result}");
	}
	else
	{
		Console.WriteLine($"Error: {response.ReasonPhrase}");
	}
}

Authorization using HTTP Query string parameters

ASP.NET Core query parameters can also be used inside an AuthorizationHandler in almost the same way as the route parameter. The IHttpContextAccessor is used to get the HttpContext. Then the Query request property can be used to access the parameters. In this demo, the query parameter is named fruit which can be used to retrieve the value. If it equals oranges, the requirement will succeed using this handler.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;

namespace AppAuthorizationService
{
    public class ValuesCheckQueryParameterHandler : AuthorizationHandler<ValuesCheckQueryParamRequirement>
    {
        private readonly IHttpContextAccessor _httpContextAccessor;

        public ValuesCheckQueryParameterHandler(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
        }

        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ValuesCheckQueryParamRequirement requirement)
        {
            var queryString = _httpContextAccessor.HttpContext.Request.Query;
            var fruit = queryString["fruit"];

            if (fruit.ToString() == "orange")
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

The controller class can then be authorized with the Authorize attribute using the ValuesQueryPolicy which checks for the requirement, which was used in the ValuesCheckQueryParameterHandler.

[Authorize("ValuesQueryPolicy")]
[ProducesResponseType(StatusCodes.Status200OK)]
[HttpGet]
[Route("q/{user}", Name = nameof(GetWithQueryParam))]
public IActionResult GetWithQueryParam([FromRoute]string user, [FromQuery]string fruit)
{
	return Ok($"get this data [{fruit}] using the query parameter");
}

A HttpClient can then be used to send the HTTP request with the query string parameter.

private static async Task CallApiwithQueryStringParam(string currentAccessToken, string fruit)
{
	_apiClient.SetBearerToken(currentAccessToken);
	var response = await _apiClient.GetAsync($"/api/values/q?fruit={fruit}");

	if (response.IsSuccessStatusCode)
	{
		var result = await response.Content.ReadAsStringAsync();
		Console.WriteLine( $"\n{result}");
	}
	else
	{
		Console.WriteLine($"Error: {response.ReasonPhrase}");
	}
}

Authorization using the HTTP Request Body

The body of a HTTP POST request can also be used to do authorization. This works slightly different to the previous two examples. The ValuesCheckRequestBodyHandler implements the AuthorizationHandler with the ValuesRequestBodyRequirement requirement and also the BodyData resource. This resource is used to do the authorization checks as required.

using Microsoft.AspNetCore.Authorization;
using System.Threading.Tasks;

namespace AppAuthorizationService
{
    public class ValuesCheckRequestBodyHandler : AuthorizationHandler<ValuesRequestBodyRequirement, BodyData>
    {
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, 
              ValuesRequestBodyRequirement requirement, BodyData bodyData)
        {
            if(bodyData.User == "mike")
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }

    public class BodyData
    {
        public string User { get; set; }
    }
}

In the Controller, the Authorize attribute is not used. This is because we do not want to deserialize the body a second time. Once inside the controller method, the body data, which has already been serialized is passed as a parameter to the authorization check. This has the disadvantage that the authorization is executed later in the pipeline. The authorization is then used in the method and a Forbidden 403 is returned, if the body data sent has unauthorized values.

[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[Produces(typeof(BodyData))]
[HttpPost]
[Route("", Name = nameof(Post))]
public async Task<IActionResult> Post([FromBody]BodyData user)
{
	var requirement = new ValuesRequestBodyRequirement();
	var resource = user;

	var authorizationResult =
		await _authorizationService.AuthorizeAsync(
			User, resource, requirement);

	if (authorizationResult.Succeeded)
	{
		return Ok($"posted this data [{user.User}] using the body");
	}
	else
	{
		return new ForbidResult();
	}
}

It is really easy to use the different parts of the HTTP request, to do the specific authorization as required.

Links:

https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies

https://docs.microsoft.com/en-us/aspnet/core/security/?view=aspnetcore-3.0

https://docs.microsoft.com/en-us/aspnet/core/razor-pages

Requiring MFA for Admin Pages in an ASP.NET Core Identity application

$
0
0

This article shows how MFA could be forced on users to access sensitive pages within an ASP.NET Core Identity application. This could be useful for applications where different levels of access exist for the different identities. For example, users might be able to view the profile data using a password login, but an administrator would be required to use MFA to access the admin pages.

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

Blogs in this series

Extending the Login with a MFA claim

The application is setup using ASP.NET Core with Identity and Razor Pages. In this demo, the SQL Server was replaced with SQLite, and the nuget packages were updated. The AddIdentity method is used instead of AddDefaultIdentity one, so we can add an IUserClaimsPrincipalFactory implementation to add claims to the identity after a successful login.

public void ConfigureServices(IServiceCollection services)
{
	services.AddDbContext<ApplicationDbContext>(options =>
		options.UseSqlite(
			Configuration.GetConnectionString("DefaultConnection")));

	//services.AddDefaultIdentity<IdentityUser>(
	//    options => options.SignIn.RequireConfirmedAccount = true)
	//    .AddEntityFrameworkStores<ApplicationDbContext>();

	services.AddIdentity<IdentityUser, IdentityRole>(
		options => options.SignIn.RequireConfirmedAccount = false)
	 .AddEntityFrameworkStores<ApplicationDbContext>()
	 .AddDefaultTokenProviders();

	services.AddSingleton<IEmailSender, EmailSender>();
	services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>, AdditionalUserClaimsPrincipalFactory>();

	services.AddAuthorization(options =>
	{
		options.AddPolicy("TwoFactorEnabled",
			x => x.RequireClaim("TwoFactorEnabled", "true" )
		) ;
	});

	services.AddRazorPages();
}

The AdditionalUserClaimsPrincipalFactory adds the TwoFactorEnabled claim to the user claims after a successful login. This is only added after a login. The value is read from the database. This is added here because the user should only access the higher protected view, if the identity has logged in with MFA. If the database view was read from the database directly instead of using the claim, it would be possible to access the view without MFA directly after activating the MFA.

using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;

namespace IdentityStandaloneMfa
{
    public class AdditionalUserClaimsPrincipalFactory : UserClaimsPrincipalFactory<IdentityUser, IdentityRole>
    {
        public AdditionalUserClaimsPrincipalFactory( 
            UserManager<IdentityUser> userManager,
            RoleManager<IdentityRole> roleManager, 
            IOptions<IdentityOptions> optionsAccessor) 
            : base(userManager, roleManager, optionsAccessor)
        {
        }

        public async override Task<ClaimsPrincipal> CreateAsync(IdentityUser user)
        {
            var principal = await base.CreateAsync(user);
            var identity = (ClaimsIdentity)principal.Identity;

            var claims = new List<Claim>();

            if (user.TwoFactorEnabled)
            {
                claims.Add(new Claim("TwoFactorEnabled", "true"));
            }
            else
            {
                claims.Add(new Claim("TwoFactorEnabled", "false")); ;
            }

            identity.AddClaims(claims);
            return principal;
        }
    }
}

Because we changed the Identity service setup in the Startup class, the layouts of the Identity need to be updated. Scaffold the Identity pages into the application. Define the layout in the Identity/Account/Manage/_Layout.cshtml file.

@{
    Layout = "/Pages/Shared/_Layout.cshtml";
}

Also add the _layout for all the manage pages from the Identity Pages.

@{
    Layout = "_Layout.cshtml";
}

Validation the MFA requirement in the Admin Page

The admin Razor Page validates that the user has logged in using MFA. In the OnGet method, the Identity is used to access the user claims. The TwoFactorEnabled claim is checked for the value true. If the user has not this claim, the page will redirect to the Enable MFA page. This is possible because the user has logged in already, but without MFA.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace IdentityStandaloneMfa
{
    public class AdminModel : PageModel
    {
        public IActionResult OnGet()
        {
            var claimTwoFactorEnabled = User.Claims.FirstOrDefault(t => t.Type == "TwoFactorEnabled");

            if (claimTwoFactorEnabled != null && "true".Equals(claimTwoFactorEnabled.Value))
            {
                // You logged in with MFA, do the admin stuff
            }
            else
            {
                return Redirect("/Identity/Account/Manage/TwoFactorAuthentication");
            }

            return Page();
        }
    }
}

UI logic to show hide information about the user login

An Authorization policy was added in the startup which requires the TwoFactorEnabled claim with the value true.

services.AddAuthorization(options =>
{
	options.AddPolicy("TwoFactorEnabled",
		x => x.RequireClaim("TwoFactorEnabled", "true" )
	) ;
});

This policy can then be used in the _Layout view to show or hide the Admin menu with the warning.

@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager
@inject IAuthorizationService AuthorizationService

If the identity has logged using MFA, then the Admin menu will be displayed without the warning. If the user has logged without the MFA, the font awesome icon will be displayed, and the tooltip which informs the user, explaining the warning.

@if (SignInManager.IsSignedIn(User))
{
	@if ((AuthorizationService.AuthorizeAsync(User, "TwoFactorEnabled")).Result.Succeeded)
	{
		<li class="nav-item">
			<a class="nav-link text-dark" asp-area="" asp-page="/Admin">Admin</a>
		</li>
	}
	else
	{
		<li class="nav-item">
			<a class="nav-link text-dark" asp-area="" asp-page="/Admin" 
			   id="tooltip-demo"  
			   data-toggle="tooltip" 
			   data-placement="bottom" 
			   title="MFA is NOT enabled. This is required for the Admin Page. If you have activated MFA, then logout, login again.">
				<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
				Admin
			</a>

		</li>
	}
}

If the user logins without MFA , then the warning is displayed.

And when the user clicks the admin link, then the user is redirected to the MFA enable view.

Links:

https://tools.ietf.org/html/draft-ietf-oauth-amr-values-04

https://openid.net/specs/openid-connect-core-1_0.html

Forcing reauthentication with Azure AD

https://docs.microsoft.com/en-us/azure/active-directory/authentication/concept-mfa-howitworks

https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc

Viewing all 169 articles
Browse latest View live