Implementing (CAC) certificate authentication in ASP.Net Core Razor app (program.asp)

74 Views Asked by At

The MS documentation on certificate authentication is a bit bewildering to me as a relatively new Core developer, and the code that I've cobbled together doesn't seem actually doesn't seem to work correctly - I'm able to get it to prompt me for my cert, but it doesn't seem to want to want to authenticate it. What I'm basically trying to do is to prompt the user for a certificate, and if they escape out of that prompt they go to the log in page, which does work, but if they choose their cert, it should check to see if their EDIPI from the cert is in the database, and if it isn't redirect them to the login page, and if it is, log them in. Here is what I have for my AddAuthentication code (in program.cs as I don't use a startup.cs):

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
    // Your existing cookie configuration

    options.LoginPath = "/Identity/Account/Login";
})
.AddCertificate("CertificateScheme", options =>
{
    options.Events = new CertificateAuthenticationEvents
    {
        OnCertificateValidated = async context =>
        {
            var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Program>>();
            logger.LogInformation("Starting certificate validation.");

            var userService = context.HttpContext.RequestServices.GetRequiredService<IUserService>();
            var ediNumber = Utilities.ExtractEdiNumberFromCertificate(context.ClientCertificate);
            logger.LogInformation($"EDIPI extracted: {ediNumber}");

            var user = await userService.FindByEdipiAsync(ediNumber);
            if (user != null)
            {
                logger.LogInformation($"User found: {user.UserName}. Authenticating.");
                // Proceed with creating principal and calling context.Success()
            }
            else
            {
                logger.LogWarning("User not found with EDIPI. Failing authentication.");
                context.Fail("No matching user found.");
            }
        }
    };

});

And I've created the following Middleware, which does seem to be executed, but by that point the user hasn't been authenticated so it always goes to the else clause:

app.Use(async (context, next) =>
{
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Middleware Check: Start");

// Check if the current request is for the login page or the forgot password page
bool isLoginPage = context.Request.Path.StartsWithSegments("/Identity/Account/Login");
bool isForgotPasswordPage = context.Request.Path.StartsWithSegments("/Identity/Account/Login?authenticate=yes");

if (context.User.Identity.IsAuthenticated)
{
    logger.LogInformation("User is authenticated");
    if (context.User.HasClaim(c => c.Type == "EDIPI"))
    {
        logger.LogInformation("User authenticated via certificate with EDIPI");
        // Ensure we don't redirect if we're already on the target page
        if (!isForgotPasswordPage)
        {
            context.Response.Redirect("/Identity/Account/Login?authenticate=yes");
            return;
        }
    }

}
else
{
    logger.LogInformation("User is not authenticated");
    // This check ensures you're not redirecting API calls, non-interactive requests, or if already on the login page
    if (!isLoginPage && !context.Request.Path.StartsWithSegments("/api") && context.Request.Headers["Accept"].ToString().Contains("text/html"))
    {
        logger.LogInformation("Redirecting to login page");
        context.Response.Redirect("/Identity/Account/Login");
        return;
    }
}

await next();
});

I know I'm not actually logging the user in yet, and my redirect when the cert is authenticating is just using a query string add on for now, but this is just the initial shot and I will build from here.

0

There are 0 best solutions below