Two authentication schemes (JWT or certificate) to one endpoint route in ASP.NET Core

34 Views Asked by At

I am trying to make a GET request by certificate or JWT to the same controller endpoint, but it always seems to trigger the certificate one.

My JWT authentication looks currently like that:

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
    {
        options.Authority = builder.Configuration["Jwt:Issuer"];
        options.Audience = builder.Configuration["Jwt:Audience"];
        options.TokenValidationParameters = new TokenValidationParameters
        {
            // Configure token validation parameters
            IncludeTokenOnFailedValidation = true,
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = false,
            ValidateIssuerSigningKey = false,
            ClockSkew = TimeSpan.Zero,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
        };

        options.Events = new JwtBearerEvents
        {
            OnTokenValidated = context =>
            {
                    // Add custom claims to the principal if the token is valid
                    var claims = new[]
                        {
                            new Claim(
                                ClaimTypes.NameIdentifier,
                                context.Scheme.Name,
                                ClaimValueTypes.String, context.Options.ClaimsIssuer),
                            new Claim(ClaimTypes.Role, "Claim_AdminPrivilege_API_jwt_Read")
                        };

                    context.Principal = new ClaimsPrincipal(
                        new ClaimsIdentity(claims, context.Scheme.Name));
                    
                    // Optionally, you can access and modify other properties of the context here
                    
                    return Task.CompletedTask;
            },

            OnAuthenticationFailed = context =>
            {
                if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
                {
                    context.Response.Headers.Add("Token-Expired", "true");
                }
                return Task.CompletedTask;
            }
        };
    });

And my certificate looks like this:

builder.Services.AddSingleton<CertificateValidationService>();

// Certificate authentication in ASP.NET Core
builder.Services
    .AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
    .AddCertificate(options =>
        {
            options.RevocationMode = X509RevocationMode.NoCheck;
            options.AllowedCertificateTypes = CertificateTypes.All;
            options.Events = new CertificateAuthenticationEvents
            {
                OnCertificateValidated = context =>
                {
                    var validationService = context.HttpContext.RequestServices.GetService<CertificateValidationService>();
                    string certificate = builder.Configuration.GetSection("Certificate").GetValue<string>("File") ?? string.Empty;
                    string key = builder.Configuration.GetSection("Certificate").GetValue<string>("Key") ?? string.Empty;
                    if (validationService!= null && validationService.ValidateCertificate(context.ClientCertificate, certificate, key))
                    {
                        Console.WriteLine("Success");

                        var claims = new[]
                        {
                            new Claim(
                                ClaimTypes.NameIdentifier,
                                context.ClientCertificate.Subject,
                                ClaimValueTypes.String, context.Options.ClaimsIssuer),
                            new Claim(ClaimTypes.Role, "Claim_AdminPrivilege_API_Read")
                        };

                        context.Principal = new ClaimsPrincipal(
                            new ClaimsIdentity(claims, context.Scheme.Name));

                        context.Success();
                    }
                    else
                    {
                        Console.WriteLine("invalid cert");
                        context.Fail("invalid cert");
                    }
                    return Task.CompletedTask;
                }
            };
    });

In both cases I build the claim that later I am using for the policy:

options.AddPolicy("Role_AdminPrivilege_API", p => {
    // Add the authentication scheme for certificate
    //p.AuthenticationSchemes.Add(CertificateAuthenticationDefaults.AuthenticationScheme);
    p.AddAuthenticationSchemes(CertificateAuthenticationDefaults.AuthenticationScheme, JwtBearerDefaults.AuthenticationScheme);
    //p.RequireAssertion(context => true);
    //p.RequireClaim(ClaimTypes.Role, "Claim_AdminPrivilege_API_Read");
    p.RequireAssertion(context =>
                context.User.HasClaim(c => c.Type == ClaimTypes.Role && 
                (c.Value == "Claim_AdminPrivilege_API_Read" || c.Value == "Claim_AdminPrivilege_API_jwt_Read") ));
});

but this policy seems always just to use the certificate authentication method unless I create second function with another policy only for JWT.

When I call the web request with the JWT token I get the error:

Interop+OpenSsl+SslException: Operation failed with error - 14.

at System.Net.Security.SslStream.RenegotiateAsync[TIOAdapter](CancellationToken cancellationToken)

This is my controller function:

//[Authorize(Roles = "Claim_AdminPrivilege_API_Read,Claim_AdminPrivilege_API_jwt_Read")]
[Authorize(Policy = "Role_AdminPrivilege_API_Read")]
[HttpGet("GetEndpointAdmin/{hostname}")]
public async Task<IActionResult> GetEndpointAdmin(string hostname)

I also never see that the JWT authentication method gets triggered.

Is this idea, having two different authentications working with a same policy, even possible or do I have to create GetEndpointAdminJwt function with its own policy?

Thanks

Not sure if it is maybe related to the Kestrl server:

builder.Services.Configure<KestrelServerOptions>(options =>
    {
        
        options.ConfigureHttpsDefaults(options =>
        {
            options.AllowAnyClientCertificate();
            options.CheckCertificateRevocation = false;
            //options.ClientCertificateMode = ClientCertificateMode.DelayCertificate;
            
        });
    });

But when I say no NoCertificate Jwt works, but of course certificate is not working anymore at all.

Edit:

Checking the header on the OnAuthenticationFailed in the CertificateAuthenticationEvents event shows that the header of jwt is Bearer but still gets forwarded to the certificate section.

0

There are 0 best solutions below