Search code examples
c#azureasp.net-coreauthenticationcertificate

Implement two authentication options (Token and Certificate) in ASP Net Core


[Target netcoreapp3.1]

Hi there! So I have this Web Api that is protected by a middleware of this form in my Startup.cs:


public void ConfigureServices(IServiceCollection services)
{
    //other services configuration
    services.AddProtectedWebApi(options => { /* config */};
    //other services configuration
}

This verifies Jwt Tokens issued by Azure and grants access to the API; it works fine. At present, I have a front-end angular client website where a user signs in via Azure AD. Angular sends the token to my web API and everything works.

I would now like to use the same webapp to handle query requests from a user without credentials, but with a client certificate that would have been provided in advance. So basically, I'd like to authenticate on my Angular WebSite via Azure OR via a client cert. Angular would then follow up the information to my webapp, which would in turn authenticate the user with the appropriate method.

To be clear, I still want someone to be able to log in without a certificate by using his Azure account.

Is there a simple way to have two authentication options in this case without having to create a separate webapp? I read a bit there : https://learn.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-3.1#optional-client-certificates But it seems it'd only work on the preview of ASP.NET Core 5, which I can't use in my situation.


Solution

  • Hope what follows will help someone! I eventually found this link : https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme?view=aspnetcore-3.1

    It explains how to implement multiple authorization policies that both have a chance to succeed. Below is the solution I found using IIS after a bit more research:

    Startup.cs

    public void ConfigureServices(IServiceCollection services)
    {
        //other services configuration
    services.Configure<IISOptions>(options =>
                {
                    options.ForwardClientCertificate = true;
                });
    
                services.Configure<CertificateForwardingOptions>(options =>
                {
                    options.CertificateHeader = {/*your header present in client request*/};
                });
        //other services configuration
    services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
    .AddCertificate(options =>
                {
                    options.AllowedCertificateTypes =/*Whatever you need*/;
                    options.Events = new CertificateAuthenticationEvents
                    {
                        OnCertificateValidated = context =>
                        {
    
                            if ({/*CertValidationClass*/}.ValidateCertificate(context.ClientCertificate))
                            {
                                context.Success();
                            }
                            else
                            {
                                context.Fail("invalid cert");
                            }
    
                            return Task.CompletedTask;
                        }
                    };
                });
    
    
        services.AddProtectedWebApi(options => { /* config */};
        //other services configuration
    }
    

    {CertValidationClass} being a service or helper class custom made to verify all I have to verify to approve the certificate. Obviously you can add a lot more verifying and actions on your own to this template.

    I already had app.UseAuthentication(); app.UseAuthorization(); in my middleware pipeline, no need to change that, but you do have to add app.UseCertificateForwarding(); before these two.

    Now I just had to specify above the controller I wanted to protect that I wanted to use both Authorization methods, and just like that, if one fails, it falls back on the other and it works perfectly, I tested by making requests via Insomnia with/without tokens and with/without certficates.

    MyApiController.cs

    [Authorize(AuthenticationSchemes = AuthSchemes)]
        public class MyApiController
        {
            //Just add the schemes you want used here
            private const string AuthSchemes =
                CertificateAuthenticationDefaults.AuthenticationScheme; + "," +
                JwtBearerDefaults.AuthenticationScheme;