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>
    <version>{project-version}</version>
</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 jakarta.security.auth.callback.Callback;
import jakarta.security.auth.callback.CallbackHandler;
import jakarta.security.auth.callback.NameCallback;
import jakarta.security.auth.callback.PasswordCallback;
import jakarta.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 jakarta.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 jakarta.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 jakarta.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

If the Kerberos extension is enabled.

Environment variable: QUARKUS_KERBEROS_ENABLED

boolean

true

If DevServices has been explicitly enabled or disabled.

When DevServices is enabled Quarkus will attempt to automatically configure and start Kerberos when running in Dev or Test mode and when Docker is running.

Environment variable: QUARKUS_KERBEROS_DEVSERVICES_ENABLED

boolean

true

The container image name to use, for container based DevServices providers. See https://github.com/kerberos-io/kerberos-docker.

Environment variable: QUARKUS_KERBEROS_DEVSERVICES_IMAGE_NAME

string

gcavalcante8808/krb5-server

Indicates if the Kerberos container managed by Quarkus Dev Services is shared. When shared, Quarkus looks for running containers using label-based service discovery. If a matching container is Kerberos, it is used, and so a second one is not started. Otherwise, Dev Services for Kerberos starts a new container.

The discovery uses the quarkus-dev-service-label label. The value is configured using the service-name property.

Container sharing is only used in dev mode.

Environment variable: QUARKUS_KERBEROS_DEVSERVICES_SHARED

boolean

true

The value of the quarkus-dev-service-kerberos label attached to the started container. This property is used when shared is set to true. In this case, before starting a container, Dev Services for Kerberos looks for a container with the quarkus-dev-service-kerberos label set to the configured value. If found, it will use this container instead of starting a new one. Otherwise it starts a new container with the quarkus-dev-service-kerberos label set to the specified value.

Container sharing is only used in dev mode.

Environment variable: QUARKUS_KERBEROS_DEVSERVICES_SERVICE_NAME

string

quarkus-kerberos

The JAVA_OPTS passed to the keycloak JVM

Environment variable: QUARKUS_KERBEROS_DEVSERVICES_JAVA_OPTS

string

The Kerberos realm.

Environment variable: QUARKUS_KERBEROS_DEVSERVICES_REALM

string

JAAS Login context name. If this property 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. Use 'java.security.auth.login.config' system property to point to this JAAS configuration file. Note this property will be ignored if a custom io.quarkiverse.kerberos.ServicePrincipalSubjectFactory is registered, and it creates a non-null service Subject for the current authentication request.

Environment variable: QUARKUS_KERBEROS_LOGIN_CONTEXT_NAME

string

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

Environment variable: QUARKUS_KERBEROS_DEBUG

boolean

false

Points to a service principal keytab file and will be used to set a JAAS configuration 'keyTab' property. Note this property is only effective when loginContextName is not set. and the JAAS configuration is created automatically.

Environment variable: QUARKUS_KERBEROS_KEYTAB_PATH

string

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".

Environment variable: QUARKUS_KERBEROS_SERVICE_PRINCIPAL_NAME

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.

Environment variable: QUARKUS_KERBEROS_SERVICE_PRINCIPAL_REALM

string

Service principal password. Set this property only if using keytabPath, custom CallbackHandler or ServicePrincipalSubjectFactory is not possible.

Environment variable: QUARKUS_KERBEROS_SERVICE_PRINCIPAL_PASSWORD

string

Specifies whether to use Spnego or Kerberos OID.

Environment variable: QUARKUS_KERBEROS_USE_SPNEGO_OID

boolean

true

The Kerberos user principals map containing the principal name and password pairs. If this map is empty then two principals, 'alice' and 'bob' with the passwords matching their names will be created.

Environment variable: QUARKUS_KERBEROS_DEVSERVICES_PRINCIPALS__PRINCIPALS_

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 jakarta.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 this property 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. Use 'java.security.auth.login.config' system property to point to this JAAS configuration file. Note this property will be ignored if a custom io.quarkiverse.kerberos.client.UserPrincipalSubjectFactory is registered and creates a non-null client Subject.

Environment variable: KERBEROS_CLIENT_LOGIN_CONTEXT_NAME

string

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

Environment variable: KERBEROS_CLIENT_DEBUG

boolean

false

Points to a user principal keytab file and will be used to set a JAAS configuration 'keyTab' property. Note this property is only effective when loginContextName is not set. and the JAAS configuration is created automatically.

Environment variable: KERBEROS_CLIENT_KEYTAB_PATH

string

User Principal name.

Environment variable: KERBEROS_CLIENT_USER_PRINCIPAL_NAME

string

required

Kerberos User Principal Realm Name. If this property is set then it will be added to the user 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.

Environment variable: KERBEROS_CLIENT_USER_PRINCIPAL_REALM

string

Service Principal name

Environment variable: KERBEROS_CLIENT_SERVICE_PRINCIPAL_NAME

string

HTTP/localhost

User principal password. Set this property only if using keytabPath, custom CallbackHandler or UserPrincipalSubjectFactory is not possible.

Environment variable: KERBEROS_CLIENT_USER_PRINCIPAL_PASSWORD

string

Specifies whether to use Spnego or Kerberos OID.

Environment variable: KERBEROS_CLIENT_USE_SPNEGO_OID

boolean

true