Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEAT: add certificate based authentication filter & attribute #43

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
ec10d11
FEAT: add certificate based authentication filter & attribute
May 26, 2019
d8bed45
TEST: certificate issuername and combined validation
Jun 12, 2019
0597836
PR-ADD: use private class for the client certificate configuration
Jun 12, 2019
273a2d5
TEST: correct subject/issuer name generation for self-signed certificate
Jun 12, 2019
0db51cd
TEST: global certificate based authentication
Jun 13, 2019
0fe9818
PR-SUG: extract inner-class in TestApiServer to configure the TLS cli…
Jun 13, 2019
a3bf894
PR-SUG: rename 'X509Validation' to 'X509ValidationRequirement'
Jun 13, 2019
891a108
PR-STYLE: update the certificate authentication filter to pass-along …
Jun 19, 2019
f14a3a6
PR-TEST: certificate authentication with thumbprint testing
Jun 19, 2019
62df27b
PR-SUG: add logging via 'ILoggerFactory' given via HttpContext
Jun 19, 2019
194fccc
PR-SUG: use configuration keys i.o. actual values for setting certifi…
Jun 19, 2019
d86681b
PR-SUG: use 'ISecretProvider' as basis for configurable certificate …
Jun 20, 2019
ced8ec1
PR-DOC: add docs for certificate authentication
Jun 20, 2019
81021ff
PR-DEL: remove the configuration keys addition in the TestApiServer
Jun 20, 2019
f112af3
PR-FIX: alter the guard predicate & message for the configuration keys
Jun 20, 2019
aa67caf
Update docs/features/certificate-authentication.md
stijnmoreels Jun 21, 2019
4090702
PR-DOC: move authentication mechanisems to '/features/auth'
Jun 21, 2019
16af314
Merge branch 'feature/certificate-authentication' of https://github.c…
Jun 21, 2019
68defc0
PR-SUG: rename 'IsAllowedCertificate' > 'IsCertificateAllowed'
Jun 21, 2019
0f7eca4
PR-SUG: provide way to configure each requirement via diff validation…
Jun 25, 2019
5d5d7d9
PR-DOC: update docs with new certificate validation location setup
Jun 25, 2019
c79478f
PR-DOC: add line break in introduction text
Jul 3, 2019
4b8c2c6
Merge remote-tracking branch 'upstream/master' into feature/certifica…
Jul 3, 2019
dda914b
PR-DOC: remove '-authentication' of certificate auth. doc
Jul 3, 2019
8dd83b6
PR-SUG: restructure private methods in order to have less arguments
Jul 3, 2019
b841862
PR-SUG: extract 'GetLoggerOrDefault' in authentication filter
Jul 3, 2019
36bc344
PR-SUG: extract 'GetLoggerOrDefault' in authentication filter
Jul 3, 2019
b638cf0
PR-FIX: guard against miss-implementations of certificate locations
Jul 3, 2019
c99223d
PR-FIX: guard against miss-client.certificates
Jul 3, 2019
739ad53
PR-DOC: rename and document more the members with XML docs
Jul 3, 2019
04deb5b
PR-FIX: rename with 'ExpectedValue' in location signature
Jul 3, 2019
de9c48a
PR-SUG: use dedicated config object to set and retrieve the expected …
Jul 3, 2019
3ca9d91
PR-ADD: reconsider guards on every public/internal member
Jul 3, 2019
476b21d
PR-DOC: update certificate authentication docs with new authenticatio…
Jul 3, 2019
efeafbd
PR-DEL: remove vscode cache
Jul 4, 2019
69cd88f
PR-FIX: make validation requirement internal
Jul 4, 2019
4e87799
PR-SUG: use 'if' statement instead of expression-based null-check
Jul 4, 2019
61aedc0
PR-SUG: extract lamdba function that switches between all certificate…
Jul 8, 2019
26edbe9
PR-SUG: introduce textbook builder pattern
Jul 8, 2019
d3d8d88
PR-ADD: add remarks about registration of certificate validator
Jul 8, 2019
96b01e4
PR-ADD: extra guard against invalid values in the validation location
Jul 9, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions docs/features/auth/certificate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
---
title: "Authentication with certificate via ASP.NET Core authentication filters"
layout: default
---

The `Arcus.WebApi.Security` package provides a mechanism that uses the client certificate of the request to grant access to a web application.

This authentication process consists of following parts:

1. Find the client certificate configured on the HTTP request
2. Determine which properties of the received client certificate are used for authentication
3. The property value(s) of the client certificate matches the value(s) determined via configured secret provider, configuration or custom implementation

The package allows two ways to configure this type of authentication mechanism in an <span>ASP.NET</span> application:
- [Globally enforce certificate authentication](#Globally-enforce-certificate-authentication)
- [Enforce certificate authentication per controller or operation](#Enforce-certificate-authentication-per-controller-or-operation)

## Globally enforce certificate authentication

### Introduction

The `CertificateAuthenticationFilter` can be added to the request filters in an <span>ASP.NET</span> Core application.
This filter will then add authentication to all endpoints via one or many certificate properties configurable on the filter itself.

### Usage

The authentication requires a service dependency to be registered with the services container of the <span>ASP.NET</span> request pipeline, which can be one of the following:
- `ICachedSecretProvider` or `ISecretProvider`: built-in or you implementation of the secret provider.
- `Configuration`: key/value pairs in the configuration of the <span>ASP.NET</span> application.
- `IX509ValidationLocation`/`X509ValidationLocation`: custom or built-in implementation that retrieves the expected certificate values.

This registration of the service is typically done in the `ConfigureServices` method of the `Startup` class.

Each certificate property that should be validated can use a different service dependency.
This mapping of what service which property uses, is defined in an `CertificateAuthenticationValidator` instance.

Once this is done, the `CertificateAuthenticationFilter` can be added to the filters that will be applied to all actions:

```csharp
public void ConfigureServices(IServiceCollections services)
{
services.AddScoped<ICachedSecretProvider(serviceProvider => new MyCachedSecretProvider());

var certificateAuthenticationConfig =
new CertificateAuthenticationConfigBuilder()
.WithIssuer(X509ValidationLocation.SecretProvider, "key-to-certificate-issuer-name")
.Build();

services.AddScoped<CertificateAuthenticationValidator>(
serviceProvider => new CertificateAuthenticationValidator(certificateAuthenticationConfig));

services.AddMvc(
options => options.Filters.Add(new CertificateAuthenticationFilter()));
}
```

## Enforce certificate authentication per controller or operation

### Introduction

The `CertificateAuthenticationAttribute` can be added on both controller- and operation level in an <span>ASP.NET</span> Core application.
This certificate authentication will then be applied to the endpoint(s) that are decorated with the `CertificateAuthenticationAttribute`.

### Usage

The authentication requires a service dependency to be registered with the services container of the <span>ASP.NET</span> request pipeline, which can be one of the following:
- `ICachedSecretProvider` or `ISecretProvider`: built-in or you implementation of the secret provider.
- `Configuration`: key/value pairs in the configuration of the <span>ASP.NET</span> application.
- `IX509ValidationLocation`/`X509ValidationLocation`: custom or built-in implementation that retrieves the expected certificate values

This registration of the service is typically done in the `ConfigureServices` method of the `Startup` class:

```csharp
public void ConfigureServices(IServiceCollections services)
{
services.AddScoped<ICachedSecretProvider(serviceProvider => new MyCachedSecretProvider());

var certificateAuthenticationConfig =
new CertificateAuthenticationConfigBuilder()
.WithIssuer(X509ValidationLocation.SecretProvider, "key-to-certificate-issuer-name")
.Build();

services.AddScoped<CertificateAuthenticationValidator>(
serviceProvider => new CertificateAuthenticationValidator(certificateAuthenticationConfig));

services.AddMvc();
}
```

After that, the `CertificateAuthenticationAttribute` attribute can be applied on the controllers, or if more fine-grained control is needed, on the operations that requires authentication:

```csharp
[ApiController]
[CertificateAuthentication]
public class MyApiController : ControllerBase
{
[HttpGet]
[Route("authz/certificate")]
public Task<IActionResult> AuthorizedGet()
{
return Task.FromResult<IActionResult>(Ok());
}
}
```
3 changes: 2 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ PM > Install-Package Arcus.WebApi.Logging

The `Arcus.WebApi.Security` package contains functionality to easily add security capabilities to an API.

- [Shared access key authentication](features/shared-access-key.md)
- [Shared access key authentication](features/auth/shared-access-key.md)
- [Certificate authentication](features/auth/certificate.md)

## Arcus.WebApi.Logging

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using Microsoft.AspNetCore.Mvc;

namespace Arcus.WebApi.Security.Authentication
{
/// <summary>
/// Authentication filter to secure HTTP requests by allowing only certain values in the client certificate.
/// </summary>
/// <remarks>
/// Please make sure you register an <see cref="CertificateAuthenticationValidator"/> instance in the request services container (ex. in the Startup).
/// </remarks>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class CertificateAuthenticationAttribute : TypeFilterAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="CertificateAuthenticationAttribute"/> class.
/// </summary>
public CertificateAuthenticationAttribute() : base(typeof(CertificateAuthenticationFilter)) { }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Arcus.WebApi.Security.Authentication.Interfaces;
using GuardNet;
using Microsoft.Extensions.Logging;

namespace Arcus.WebApi.Security.Authentication
{
/// <summary>
/// Representation of the configurable validation requirements on a <see cref="X509Certificate2"/>.
/// </summary>
public class CertificateAuthenticationConfig
{
private readonly IDictionary<X509ValidationRequirement, (IX509ValidationLocation location, ConfiguredKey configuredKey)> _locationAndKeyByRequirement;

/// <summary>
/// Initializes a new instance of the <see cref="CertificateAuthenticationConfig"/> class.
/// </summary>
/// <param name="locationAndKeyByRequirement">
/// The series of validation locations and configured keys by certificate requirements that describes how which parts of the client certificate should be validated.
/// </param>
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="locationAndKeyByRequirement"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">Thrown when the <paramref name="locationAndKeyByRequirement"/> contains any validation location or configured key that is <c>null</c>.</exception>
internal CertificateAuthenticationConfig(
IDictionary<X509ValidationRequirement, (IX509ValidationLocation location, ConfiguredKey configuredKey)> locationAndKeyByRequirement)
{
Guard.NotNull(locationAndKeyByRequirement, nameof(locationAndKeyByRequirement), "Location and key by certificate requirement dictionary cannot be 'null'");
Guard.For<ArgumentException>(
() => locationAndKeyByRequirement.Any(keyValue => keyValue.Value.location is null || keyValue.Value.configuredKey is null),
"All locations and configured keys by certificate requirement cannot be 'null'");

_locationAndKeyByRequirement = locationAndKeyByRequirement;
}

/// <summary>
/// Gets all the expected <see cref="X509Certificate2"/> values from this current configuration instance.
/// </summary>
/// <param name="services">The request services to retrieve the necessary implementations during the retrieval of each expected certificate value.</param>
/// <param name="logger">The logger used during the retrieval of each expected certificate value.</param>
/// <returns>The key/value pair of which certificate requirement to validate together with which value in the actual client certificate to expect.</returns>
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="services"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="logger"/> is <c>null</c>.</exception>
internal async Task<IDictionary<X509ValidationRequirement, ExpectedCertificateValue>> GetAllExpectedCertificateValues(IServiceProvider services, ILogger logger)
{
Guard.NotNull(services, nameof(services), "Request services cannot be 'null'");
Guard.NotNull(logger, nameof(logger), "Logger cannot be 'null'");

var expectedValuesByRequirement =
await Task.WhenAll(
_locationAndKeyByRequirement.Select(
keyValue => GetExpectedValueForCertificateRequirement(keyValue, services, logger)));

return expectedValuesByRequirement
.Where(result => result.Value != null)
.ToDictionary(result => result.Key, result => new ExpectedCertificateValue(result.Value));
}

private static async Task<KeyValuePair<X509ValidationRequirement, string>> GetExpectedValueForCertificateRequirement(
KeyValuePair<X509ValidationRequirement, (IX509ValidationLocation location, ConfiguredKey configuredKey)> keyValue,
IServiceProvider services,
ILogger logger)
{
IX509ValidationLocation location = keyValue.Value.location;
ConfiguredKey configuredKey = keyValue.Value.configuredKey;

Task<string> getExpectedAsync = location.GetExpectedCertificateValueForConfiguredKey(configuredKey.Value, services);
string expected = getExpectedAsync != null ? await getExpectedAsync : null;

if (expected == null)
{
logger.LogWarning($"Client certificate authentication failed: no configuration value found for key={configuredKey}");
}

return new KeyValuePair<X509ValidationRequirement, string>(keyValue.Key, expected);
}
}
}
Loading