Mixing optional certificate authentication with Windows authentication in ASP.NET Core

Viewed 591

I have a .NET 6 app where I need to have different endpoints reacting to different authentication schemes. I curently support two types of JWT authentication (local authentication, OIDC) along with Windows auth. Now I'm trying to add certificate authentification (the idea being a differnet authentification endpoint, which checks the supplied certificate, and if there's one, issues a bearer token so the rest of the app can keep using bearer tokens).

My authentication controller allows to fetch tokens for multiple authorization schemes. The controller has no defined authorization scheme - the schemes are then set on individual methods

[ApiController]
[Route("[controller]")]
public class AuthenticationController : ControllerBase
{
}

Then we have the ability to fetch an access token using Windows authentication, local authentification, and certificate authentification:

[Authorize(AuthenticationSchemes = NegotiateDefaults.AuthenticationScheme)]
[HttpGet("Token")]
public IActionResult GenerateToken([FromQuery] string clientId = null)
{
  var userIdentity = User?.Identity?.Name;
  //
}

And for local auth

[AllowAnonymous]
[HttpPost("authenticate")]
public IActionResult Authenticate([FromBody] AuthenticateModel model, [FromQuery] string clientId = null)
{
  // validate login, issue token
}

Now I've added a third authentication method that looks at a certificate

[Authorize(AuthenticationSchemes = CertificateAuthenticationDefaults.AuthenticationScheme)]
[HttpGet("Certificate")]
public IActionResult GenerateCertificateToken([FromQuery] string clientId = null)
{
    var cert = HttpContext.Connection.ClientCertificate;
    // get identity from certificate, validate, issue token
}

To support optional client certificates I'm using this code approach.

webBuilder.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(options =>
    {
        options.ClientCertificateMode = Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode.AllowCertificate;
     });
});

Most of my controllers then use the access token returned by the authentification controller's 3 methods to get a token. Sample controller:

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ApiController]
[Route("[controller]")]
public class UserController : ControllerBase
{
}

So far, all is good. Now I also have a bunch of controllers that do Windows authentification only

[Authorize(AuthenticationSchemes = Microsoft.AspNetCore.Authentication.Negotiate.NegotiateDefaults.AuthenticationScheme)]
[Route("Maintenance")]
public class MaintenanceController: BaseMaintenanceController
{
}

[ApiExplorerSettings(IgnoreApi = true)]
public class BaseMaintenanceController: ControllerBase
{
}

The MaintenanceController is in a different namespace than BaseMaintenanceController, UserController and AuthentificationController (not sure if this matters).

When I do not register certificate authentification, things work as expected (except of course I cannot use GenerateCertificateToken because the auth scheme certificate is not registered. As soon as I register for certificate authentification, my MaintenanceController only throws 403s.

Here's how I register the authentification schemes in Startup.cs 's

public void ConfigureServices(IServiceCollection services)
{
    ....
    var authBuilder = services.AddAuthentication(x =>
        {
            x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        });

    var authPolicyBuilder = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);

     authBuilder = authBuilder
           .AddNegotiate()
           .AddJwtBearer() // details omitted here
           .AddCertificate(u => 
            {
                u.AllowedCertificateTypes = CertificateTypes.All;
                u.ValidateCertificateUse = true;
                u.ValidateValidityPeriod = true;
                u.RevocationMode = X509RevocationMode.NoCheck; // for speed
            });

    services.AddAuthorization(options =>
        {
            options.DefaultPolicy = authPolicyBuilder.Build();
        });
  
}

As soon as I have the authBuilder.AddCertificate call in ConfigureServices, my MaintenanceController keeps returning 403s. Remove the call to AddCertificate and MaintenanceController works fine again.Interestingly enough, the GenerateToken method in my AuthentificationController keeps working though.

Anybody has any idea why AddCertificate kills my controllers that use NegotiateDefaults.AuthenticationScheme for the entire controller, but leaves the AuthentificationController.GenerateCertificateToken running, even though it uses the same Authorize tag?

1 Answers

I figured, I'd see if me having a controller in a different Namespace would trip things up somehow, so added a new controller that performs negotiate, and from that point on, things started working, and kept working when I removed the controller I had added. I guess the sun needed top past the mid-day point or something.

Related