Quarkus - Kerberos

Introduction

Kerberos is a network authentication protocol.

Client acquires a service ticket from Kerberos Key Distribution Center (KDC) and submits it to an application which will verify it with its service principal against KDC and grant an access if the verification has been successful.

This extension supports Kerberos version 5 with the HTTP Negotiate authentication scheme which is based on the Simple And Protected Negotiate Mechanism (SPNEGO) and the Generic Security Services Application Program Interface (GSSAPI).

Installation

If you want to use this extension, you need to add the io.quarkiverse.kerberos:quarkus-kerberos extension first. In your pom.xml file, add:

<dependency>
    <groupId>io.quarkiverse.kerberos</groupId>
    <artifactId>quarkus-kerberos</artifactId>
</dependency>

Getting Started

First you have to prepare your Kerberos environment. The description of how it should be done securely is out of scope of this document, please follow the deployment specfic policies.

However, here is a sequence of steps you can try for a quick test:

  • Install Kerberos:

Fedora: [root@server ~]# yum install krb5-server krb5-libs krb5-workstation. If you do not use Fedora then follow the OS specific instructions.

  • Edit /etc/krb5.conf - either uncomment the configuration related to EXAMPLE.COM or add a new realm, example, QUARKUS.COM. Make sure the realm’s kdc and admin_server properties point to localhost.

  • Create the database: kdb5_util create -s.

  • Start kadmin.local and add principals and keytabs in its command line:

User principal:

addprinc bob (use password bob or whatever you prefer)

Service principal:

addprinc HTTP/localhost (use password service or whatever you prefer)

Add a keytab for the service principal:

ktadd -k /etc/service.keytab HTTP/localhost

and press q to exit.

To make it easier to test you may need to do chmod og+r /etc/*.keytab since you are creating them as a root but you’ll run Quarkus App without the root permissions.

  • start KDC: systemctl start krb5kdc.service and systemctl start kadmin.service

  • Prepare a service ticket for bob: kinit bob

  • Create your Quarkus application which will use this extension. Lets assume it has a JAX-RS method with a /api/users/me path and which returns a user name. Update its application.properties to point to the service principal key tab: quarkus.kerberos.keytab-path=/etc/service.keytab.

  • Build and start the application and test it:

It should return bob.

How to configure the extension.

In many cases all you will need is to ensure the service principal password or its keytab is accessible. If you have created a keytab file then use quarkus.kerberos.keytab-path to point to it - using the keytab is recommended.

If you haven’t created a keytab just yet then you can register a custom callback handler, for example:

import jakarta.enterprise.context.ApplicationScoped;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;

import io.quarkiverse.kerberos.KerberosCallbackHandler;

@ApplicationScoped
public class UsernamePasswordCallbackHandler implements KerberosCallbackHandler {
        private final String username;
        private final char[] password;

        private UsernamePasswordCallbackHandler(final String username, final char[] password) {
            this.username = username;
            this.password = password;
        }

        @Override
        public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
            for (Callback current : callbacks) {
                if (current instanceof NameCallback) {
                    NameCallback ncb = (NameCallback) current;
                    ncb.setName(username);
                } else if (current instanceof PasswordCallback) {
                    PasswordCallback pcb = (PasswordCallback) current;
                    pcb.setPassword(password);
                } else {
                    throw new UnsupportedCallbackException(current);
                }
            }
        }
    }
}

Note io.quarkiverse.kerberos.KerberosCallbackHandler extends javax.security.auth.callback.Callback - it only acts as a marker interface for this extension to avoid having unrelated CallbackHandlers injected.

The service principal name itself is calculated from the current HTTP Host header, for example, given Host: localhost:8080 the name will be calculated as HTTP/localhost. If necessay it can be customized with quarkus.kerberos.service-principal-name.

If the KDC configuration has no default realm configured then a service principal realm can be set with quarkus.kerberos.service-principal-realm.

User Principal

You can access a user principal in the service code once the authentication has been completed, for example:

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;

import io.quarkiverse.kerberos.KerberosPrincipal;

import io.quarkus.security.Authenticated;
import io.quarkus.security.identity.SecurityIdentity;

@Path("/api/users")
@Authenticated
public class UsersResource {

    @Inject
    SecurityIdentity identity;
    @Inject
    KerberosPrincipal kerberosPrincipal;

    @GET
    @Path("/me")
    @Produces("text/plain")
    public String me() {
        return identity.getPrincipal().getName();
    }
}

For example, given bob@EXAMPLE.COM, a simple bob name will be returned. You can cast Principal to io.quarkiverse.kerberos.KerberosPrincipal or inject it directly and get a full bob@EXAMPLE.COM (or bob/admin@EXAMPLE.COM) name and the realm part of the name, EXAMPLE.COM. If the principal name contains an instance qualifier such as bob/admin then KerberosPrincipal will return admin as the role name.

JAAS Login Configuration

The extension will generate a JAAS Login Configuration by default.

However, if you have an existing JAAS Login Configuration then set quarkus.kerberos.login-context-name to point to a JAAS Configuration entry and use a `java.security.auth.login.config' system property to point to the file containing this configuration entry.

Service Principal Subject Customization

The extension will use javax.security.auth.login.LoginContext to create a Subject representing a service principal, using the auto-generated or external JAAS Login Configuration as well as the registered callback unless a keytab is used.

You can customize this process by registering a custom io.quarkiverse.kerberos.ServicePrincipalSubjectFactory:

import jakarta.enterprise.context.ApplicationScoped;
import javax.security.auth.Subject;
import io.quarkiverse.kerberos.ServicePrincipalSubjectFactory;

@ApplicationScoped
public class CustomServicePrincipalSubjectFactory implements ServicePrincipalSubjectFactory {
        @Override
        public Subject getSubjectForServicePrincipal(String servicePrincipalName) {
            ....
        }
    }
}

Dev Services for Kerberos

Quarkus Dev Services support the automatic provisioning of unconfigured services in development and test mode.

This extension provides Dev Services for Kerberos which uses a Kerberos Docker image.

Start your application in a Dev Mode with mvn quarkus:dev.

You will see in the console something similar to:

$ mvn quarkus:dev

2021-10-07 10:56:18,276 INFO  [🐳 [gcavalcante8808/krb5-server:latest]] (build-18) Creating container for image: gcavalcante8808/krb5-server:latest
...
2021-10-07 10:56:18,881 INFO  [🐳 [gcavalcante8808/krb5-server:latest]] (build-18) Container gcavalcante8808/krb5-server:latest started in PT0.621235S
...
Initializing database '/var/lib/krb5kdc/principal' for realm 'EXAMPLE.COM',
...
Principal "admin/admin@EXAMPLE.COM" created.

2021-10-07 10:56:19,149 INFO  [io.qua.ker.dep.dev.KerberosDevServicesProcessor] (build-18) Kerberos configuration file path: /tmp/devservices-krb516887219905674106017.conf, mapped KDC port: 32771, mapped admin server port: 32769
2021-10-07 10:56:19,152 INFO  [io.qua.ker.dep.dev.KerberosDevServicesProcessor] (build-18) Dev Services for Kerberos started.

HTTP/localhost service principal (with a servicepwd password) as well as alice and bob user principals (with passwords equal to their names) are created by default in a default EXAMPLE.COM realm.

Different users can be set with a quarkus.kerberos.devservices.users map property, for example, quarkus.kerberos.devservices.users.jduke=theduke, etc. The service principal can be customized with quarkus.kerberos.service-principal-name, its password - with quarkus.kerberos.service-principal-password, the realm - with either quarkus.kerberos.devservices.realm or quarkus.kerberos.service-principal-realm.

Now you can set a KRB5_CONFIG environment property pointing to the file such as /tmp/devservices-krb516887219905674106017.conf, use kinit to prepare a ticket granting ticket for a specific user and use the browser or curl to test the endpoint. Dedicated Dev UI for Dev Services For Kerberos might be offered in the future as well.

Debugging

Please enable a trace logging level for io.quarkiverse.kerberos.runtime.KerberosIdentityProvider in order to see the log messages reported by this IdentityProvider:

quarkus.log.category."io.quarkiverse.kerberos.runtime.KerberosIdentityProvider".level=TRACE
quarkus.log.category."io.quarkiverse.kerberos.runtime.KerberosIdentityProvider".min-level=TRACE

Also, if you would like to see the debug messages reported by the Kerberos system itself then make sure that quarkus.kerberos.debug is set to true if the JAAS context is auto-generated or debug is set to true in a custom JAAS context file.

Testing

You can test this extension with Dev Services for Kerberos or Apache Directory Service.

In both cases add the following dependency:

<dependency>
   <groupId>io.quarkiverse.kerberos</groupId>
   <artifactId>quarkus-kerberos-test-util</artifactId>
   <version>${version.quarkus.kerberos.test.util}</version>
   <scope>test</scope>
</dependency>

With Dev Services

You can write the test code like this when using Dev Services:

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;

import io.quarkiverse.kerberos.test.utils.KerberosTestClient;
import io.restassured.RestAssured;

public class SpnegoAuthenticationTestCase {
    public static final String WWW_AUTHENTICATE = "WWW-Authenticate";
    public static final String NEGOTIATE = "Negotiate";

    KerberosTestClient kerberosTestClient = new KerberosTestClient();

    @Test
    public void testSpnegoSuccess() throws Exception {

        var header = RestAssured.get("/identity")
                .then().statusCode(401).extract().header(WWW_AUTHENTICATE);
        assertEquals(NEGOTIATE, header);

        var result = kerberosTestClient.get("/identity", "alice", "alice");
        result.statusCode(200).body(Matchers.is("alice"));
    }
}

With Apache Directory Service

You can write the same test code you can do with Dev Services for Kerberos but you’ll also need to add a test resource initializing Apache DS:

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;

import io.quarkiverse.kerberos.test.utils.KerberosKDCTestResource;
import io.quarkiverse.kerberos.test.utils.KerberosTestClient;
import io.quarkus.test.common.QuarkusTestResource;
import io.restassured.RestAssured;

@QuarkusTestResource(KerberosKDCTestResource.class)
public class SpnegoAuthenticationTestCase {
    public static final String WWW_AUTHENTICATE = "WWW-Authenticate";
    public static final String NEGOTIATE = "Negotiate";

    KerberosTestClient kerberosTestClient = new KerberosTestClient();

    @Test
    public void testSpnegoSuccess() throws Exception {

        var header = RestAssured.get("/identity")
                .then().statusCode(401).extract().header(WWW_AUTHENTICATE);
        assertEquals(NEGOTIATE, header);

        var result = kerberosTestClient.get("/identity", "jduke", "theduke");
        result.statusCode(200).body(Matchers.is("jduke"));
    }
}

At the moment only a single jduke user is supported when testing with Apache DS and Dev Services for Kerberos have to be disabled: quarkus.kerberos.devservices.enabled=false.

With Browser

You can also configure your browser such as Firefox to use Negotiate Mechanism.

A good summary is provided here.

For example, if you run your application on the localhost then add localhost (without a port) as the only value to the Firefox about:config/network.negotiate-auth.trusted-uris property.

Next, use kinit to create a ticket granting ticket (TGT) for a selected user principal for the browser to use this TGT. Make sure kinit sees the same Kerberos KDC configuration which the browser will see for both kinit (and other Kerberos tools) and the browser to work with the same Kerberos KDC instance.

If the default Kerberos KDC configuration at /etc/krb5.conf is used then you don’t even need to restart a browser. If a custom Kerberos KDC configuration is used by kinit then point to it with KRB5_CONFIG and either update ~/.bashrc or launch the browser from the shell where KRB5_CONFIG is set.

Now open your browser and access the endpoint - the browser will do the negotiation using the created TGT without asking for a user name and password.

Extension Configuration Reference

Configuration property fixed at build time - All other configuration properties are overridable at runtime

Configuration property

Type

Default

Determine if the Kerberos extension is enabled

boolean

true

JAAS Login context name. If it is not set then the JAAS configuration will be created automatically otherwise a JAAS configuration file must be available and contain an entry matching its value - in this case use java.security.auth.login.config system property pointing to this file.

string

Specifies if a JAAS configuration debug property should be enabled. This property is only effective when loginContextName is not set and the JAAS configuration is created automatically.

boolean

false

Service principal keytab file path which will be used to set a JAAS configuration keyTab property. This property is only effective when loginContextName is not set and the JAAS configuration is created automatically.

string

Specifies if an Spnego authentication mechanism object identifier (OID) should be used for creating a GSSContext. A Kerberos OID will be used if this property is set to false.

boolean

true

Kerberos Service Principal Name. If this property is not set then the service principal name will be calculated by concatenating HTTP/ and the HTTP Host header value, for example: HTTP/localhost.

string

Kerberos Service Principal Realm Name. If this property is set then it will be added to the service principal name, for example, HTTP/localhost@SERVICE-REALM.COM. Setting the realm property is not required if it matches a default realm set in the Kerberos Key Distribution Center (KDC) configuration.

string

Kerberos Service Principal Password. Set this property only if using the keytabPath, custom CallbackHandler or ServicePrincipalSubjectFactory is not possible.

string

Client Support

In your pom.xml file, add:

<dependency>
    <groupId>io.quarkiverse.kerberos</groupId>
    <artifactId>quarkus-kerberos-client</artifactId>
</dependency>

This module provides the utility code which can be used to add Kerberos service tickets as HTTP Authorization Negotiate scheme values. It can be done with the help of the JAX-RS KerberosClientRequestFilter or directly in the application code.

Using this module can be useful when a Quarkus endpoint has to make an outbound call to a remote service requiring Kerberos Negotiate Authentication.

For example, lets assume your FrontendService application has to call to the remote IdentityService:

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import io.quarkiverse.kerberos.KerberosPrincipal;
import io.quarkus.security.Authenticated;
import io.quarkus.security.identity.SecurityIdentity;

@Path("identity")
@Authenticated
public class IdentityService {

    @Inject
    SecurityIdentity securityIdentity;

    @Inject
    KerberosPrincipal kerberosPrincipal;

    @GET
    public String getIdentity() {
        return securityIdentity.getPrincipal().getName() + " " + kerberosPrincipal.getFullName() + " "
                + kerberosPrincipal.getRealm();
    }
}

Next you can implement FrontendService.

KerberosClientRequestFilter

First MP RestClient IdentityServiceClient interface has to be created and KerberosClientRequestFilter registered:

package io.quarkiverse.kerberos.it;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import io.quarkiverse.kerberos.client.KerberosClientRequestFilter;

@RegisterRestClient
@RegisterProvider(KerberosClientRequestFilter.class)
@Path("/")
public interface IdentityServiceClientWithFilter {

    @GET
    String getIdentity();
}

Now FrontendService will look like this

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import org.eclipse.microprofile.rest.client.inject.RestClient;

@Path("frontend")
public class FrontendResource {

    @Inject
    @RestClient
    IdentityServiceClientWithFilter identityServiceClientWithFilter;

    @GET
    @Path("with-filter")
    public String getIdentityWithSimpleNegotiationInFilter() {
        return identityServiceClientWithFilter.getIdentity();
    }
}

Configure the application like this:

io.quarkiverse.kerberos.it.IdentityServiceClientWithFilter/mp-rest/url=http://localhost:8081/identity

kerberos-client.user-principal-name=jduke
kerberos-client.user-principal-password=theduke

KerberosClientSupport

If necessary you can use KerberosClientSupport directly in the application, for example:

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import org.eclipse.microprofile.rest.client.inject.RestClient;

import io.quarkiverse.kerberos.client.KerberosClientSupport;

@Path("frontend")
public class FrontendResource {
    private static final String NEGOTIATE = "Negotiate";

    @Inject
    @RestClient
    IdentityServiceClient identityServiceClient;

    @Inject
    KerberosClientSupport kerberosClientSupport;

    @GET
    @Path("with-simple-negotiation")
    public String getIdentityWithSimpleSimpleNegotiation() throws Exception {
        return identityServiceClient.getIdentity(NEGOTIATE + " " + kerberosClientSupport.getServiceTicket());
    }
}

Note IdentityServiceClient does not have KerberosClientRequestFilter registered but instead has a HeaderParam:

import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.Path;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

@RegisterRestClient
@Path("/")
public interface IdentityServiceClient {

    @GET
    String getIdentity(@HeaderParam("Authorization") String serviceTicket);
}

Multi Step Negotiation

Note that a single step Negotiation is shown in both KerberosClientRequestFilter and KerberosClientSupport sections.

Single step Negotiation should work for many practical cases however the Negotiate protocol may involve more than one exchange between the client and the server before a service ticket can be acquired. If you have to deal with such cases then you can write the application code as follows:

import java.security.PrivilegedExceptionAction;
import java.util.Base64;

import jakarta.inject.Inject;
import javax.security.auth.Subject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotAuthorizedException;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.HttpHeaders;

import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.ietf.jgss.GSSContext;

import io.quarkiverse.kerberos.client.KerberosClientSupport;

@Path("frontend")
public class FrontendResource {
    private static final String NEGOTIATE = "Negotiate";

    @Inject
    @RestClient
    IdentityServiceClient identityServiceClient;

    @Inject
    KerberosClientSupport kerberosClientSupport;

    @GET
    @Path("with-multi-step-negotiation")
    public String getIdentityWithMultiStepNegotiation() throws Exception {
        return Subject.doAs(kerberosClientSupport.getUserPrincipalSubject(), new IdentityServiceAction());
    }

    private class IdentityServiceAction implements PrivilegedExceptionAction<String> {

        @Override
        public String run() throws Exception {
            GSSContext serviceContext = kerberosClientSupport.createServiceContext();

            byte[] tokenBytes = new byte[0];

            while (!serviceContext.isEstablished()) {
                try {
                    return identityServiceClient.getIdentity(
                            NEGOTIATE + " " + kerberosClientSupport.getNegotiateToken(serviceContext, tokenBytes));
                } catch (NotAuthorizedException ex) {
                    String header = ex.getResponse().getHeaderString(HttpHeaders.WWW_AUTHENTICATE);
                    if (header != null && header.length() > NEGOTIATE.length() + 1) {
                        tokenBytes = Base64.getDecoder().decode(header.substring(NEGOTIATE.length() + 1));
                        continue;
                    }
                    throw ex;
                }
            }
            throw new RuntimeException("Kerberos ticket can not be created");
        }
    };
}

Note if the remote service requires the negotiation to continue then a new token is acquired and a new request is made to the remote service.

Configuration

Configuring KerberosClientSupport is similar to the way Kerberos support is configured on the server, see the configuration reference below.

Configuration property fixed at build time - All other configuration properties are overridable at runtime

Configuration property

Type

Default

JAAS Login context name. If it is not set then the JAAS configuration will be created automatically otherwise a JAAS configuration file must be available and contain an entry matching its value - in this case use java.security.auth.login.config system property pointing to this file.

string

Specifies if a JAAS configuration debug property should be enabled. This property is only effective when loginContextName is not set and the JAAS configuration is created automatically.

boolean

false

User principal keytab file path which will be used to set a JAAS configuration keyTab property. This property is only effective when loginContextName is not set and the JAAS configuration is created automatically.

string

Specifies if an Spnego authentication mechanism object identifier (OID) should be used for creating a GSSContext. A Kerberos OID will be used if this property is set to false.

boolean

true

Kerberos Service Principal Name.

string

HTTP/localhost

Kerberos User Principal Name. It is a required property.

string

Kerberos User Principal Realm Name. If this property is set then it will be added to the user principal name, for example, alice@SERVICE-REALM.COM. Setting the realm property is not required if it matches a default realm set in the Kerberos Key Distribution Center (KDC) configuration.

string

Kerberos User Principal Password. Set this property only if using the keytabPath, custom CallbackHandler or UserPrincipalSubjectFactory is not possible.

string