Advertisements

Adding managed kerberos to an AspNetCore application

Adding managed kerberos to an AspNetCore application

March 1, 2019 Uncategorized 2

Kerberos is a beast. No, not Cerberus the mythical 3-headed dog. We’re talking about the protocol that phonetically shares the same name. It’s been the source of multiple headaches, whether it be IIS configuration on the service machine or trusted sites configuration on the client machine. We’re going to shed some light on this issue and make kerberos negotiation available when you’re application is not running on IIS and even when it’s not running on Windows.

Disclaimer: This is for Kerberos negotiation only, not NTLM authentication.

Setting up your environment

For Kerberos authentication to work, the client machine must be on the domain that is controlled by the domain controller. So, for example, if you have an Active Directory domain controller running the internal.domain.com domain, the client machine must be connected to that domain.

For the server to work cross platform, we need to create a service account and SPN (Service Principal Name). The subject of SPNs is out of scope of this article, but there is an excellent blog post about it by Mohammed Wasay. Basically, an SPN is a link between a service account on the domain controller and a service responding on a hostname.

c:\> setspn -a HTTP/website internal.domain.com\service_account

Let’s break down the example.

Command part Description
setspn The executable used to set SPNs.
-a Add SPN to the domain controller. This can be replaced with -s, which verifies that no duplicate exists.
HTTP Defines the SPN as a principal for web services.
website The hostname of the website. Internally, on the domain, the website would respond to http(s)://website or http(s)://website.internal.domain.com.
internal.domain.com The domain controller by the domain controller.
service_account The service account to associate the SPN with.

The hostname is important for this to work. You can’t use an SPN for any other hostname other than the one it’s configured for. In other words, you can’t use an SPN for website.internal.domain.com and expect it to work on localhost. However, you can alter your HOSTS file so that your machine responds to the correct hostname.

Creating your application

Let’s create and AspNetCore application running on .Net Core.

create.png

The docker support and http settings are irrelevent for this example and can be enabled or disabled. It’s totally up to you. We have it enabled in the new project dialog, but comment it out in the Startup.cs file since we don’t have the SSL certificate for website or website.internal.domain.com.

Next you need to install the magical nuget package called Kerberos.NET. This package is a thing of beauty, mostly because the internals of Kerberos are extremely hard to understand and decipher. The package is written by Steve Syfuhs who works at Microsoft on Kerberos and other interesting stuff that requires specialized knowledge.

Configure your project and IIS

Since we have to run our application on a specific host, we’re going to be using IIS.

We won’t be going over how to configure IIS for your application and how to use visual studio to connect the debugger to IIS. That can be handled by a separate blog post.

The important  part is to set up IIS and your project to respond to the correct host name, which in our example is website or website.internal.domain.com.

project

Notice that the debug profile is not set up for Windows Authentication. This is correct. We will be handling the negotiation and handshake manually.

Create the authentication handler and options

The options are pretty straightforward. We need an SPN, a domain, and a username and password.

public class KerberosAuthenticationSchemeOptions : AuthenticationSchemeOptions
{
    public string Spn { get; set; }
    public string Domain { get; set; }
    public string UserName { get; set; }
    public string Password { get; set; }
}

The handler is where we do most of the heavy lifting. We create a class called KerberosAuthenticationHandler;

public class KerberosAuthenticationHandler 
    : AuthenticationHandler<KerberosAuthenticationSchemeOptions>

The methods we mostly care about overriding are HandleChallengeAsync and HandleAuthenticateAsync. HandleChallengeAsync is where we start the handshake. Nothing complicated here.

protected override Task HandleChallengeAsync(AuthenticationProperties properties)
{
    Response.Headers.Add("WWW-Authenticate", "Negotiate");
    Response.StatusCode = 401;
    return Task.CompletedTask;
}

HandleAuthenticateAsync is where we verify the Kerberos ticket using Kerberos.NET.

protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{           
    var authorization = Request.Headers["authorization"];
    if (authorization == StringValues.Empty) return AuthenticateResult.Fail("No authorization header");
    if (!IsNegotiate(authorization)) return AuthenticateResult.NoResult();

    var authenticator = CreateAuthenticator();

    // Kerberos.NET supports sending the ticket with the Negotiate prefix, so we don't need to do any string manipulation
    var identity = await authenticator.Authenticate(authorization);
    var principal = new ClaimsPrincipal(identity);
    var ticket = new AuthenticationTicket(principal, Scheme.Name);
    return AuthenticateResult.Success(ticket);
}

private KerberosAuthenticator CreateAuthenticator()
{
    // This could be done with DI and a factory pattern, but we're keeping it simple here.
    var principalName = new PrincipalName(PrincipalNameType.NT_PRINCIPAL, Options.Domain, new[] { Options.UserName });
    var key = new KerberosKey(Options.Password, principalName, Options.Password);
    var validator = new KerberosValidator(key);            
    var authenticator = new KerberosAuthenticator(validator);
    return authenticator;
}

private bool IsNegotiate(string authorizationHeader) =>
    authorizationHeader.StartsWith("negotiate ", StringComparison.OrdinalIgnoreCase);

That’s basically it.

How to use it

Alright, we’ve talked A LOT and done a little programming. I can just here you across the internet saying, “how the hell do I get this to work then?”

Let’s open up Startup.cs and make some edits. First, let’s register the authentication handlers that we’re going to use. We’re going to register our Kerberos handler and also the Cookie authentication handler. This is done because we don’t want to default to Kerberos authentication. That would mean that every single request would perform authentication against your domain controller, and that could could become quite heavy. I mean, you’re free to do so if you wish, but I think this is better.

Ok, so let’s look at the ConfigureServices method.

public void ConfigureServices(IServiceCollection services)
{
    services.Configure(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

    services
        .AddAuthentication(options =>
        {
            options.DefaultScheme = 
                CookieAuthenticationDefaults.AuthenticationScheme;
        })
        .AddScheme<KerberosAuthenticationSchemeOptions, KerberosAuthenticationHandler>(
            "Kerberos", 
            options =>
            {
                options.Domain = "internal.domain.com";
                options.Spn = "http/website";
                options.UserName = "service_account";
                options.Password = "service_account_password";
            }
        )
        .AddCookie(options =>
        {
            options.LoginPath = "/Negotiate";
        })
        ;

    services
        .AddMvc(options =>
        {
            options.Filters.Add(new AuthorizeFilter());
        })
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

We set up MVC to always authorize the user. You’ll also notice that there is a LoginPath that is set up in the cookie authentication handler. We’ll get to that soon enough.

The Configure method has nothing out of the ordinary.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        //app.UseHsts();
    }

    //app.UseHttpsRedirection();
    app.UseAuthentication();
    app.UseStaticFiles();
    app.UseCookiePolicy();

    app.UseMvc();
}

As you can see, we put authentication before static files to make sure that authentication is performed for them as well.

The last piece of the puzzle is the Negotiate page that was in the cookie authentication login path. We add a Razor page called Negotiate.cshtml. This could also be done with a NegotiateController and Index method if you’re using the MVC pattern.

[Authorize(AuthenticationSchemes = "Kerberos")]
public class NegotiateModel : PageModel
{
    public async Task OnGet(string returnUrl)
    {
        // Signs the user in using the default sign in scheme, which is cookie for this example
        await HttpContext.SignInAsync(User);
        Response.Redirect(returnUrl);
    }

}

Conclusion

This is pretty involved, and getting the code to work means doing a lot of up front configuration. However, once you get it to work you can do things in development like mocking out the KerberosAuthenticator in the KerberosAuthenticationHandler to just return a static ClaimsIdentity.

 

Advertisements

 

2 Responses

  1. Xavier says:

    I checked this example with my website (MVC NetCore) and it works perfectly. But now I need to call my API and I need to have the login passed to it so that the database connection is made with the same user that was logged on my website. How do I do it?

    • gislikonrad says:

      I would go a different route with that. I would use something like IdentityServer4 that would implement the kerberos authentication and return a jwt token which would be usable with the api.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.