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 toEXAMPLE.COMor add a new realm, example,QUARKUS.COM. Make sure the realm’skdcandadmin_serverproperties point tolocalhost. -
Create the database:
kdb5_util create -s. -
Start
kadmin.localand 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.serviceandsystemctl 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/mepath and which returns a user name. Update itsapplication.propertiesto point to the service principal key tab:quarkus.kerberos.keytab-path=/etc/service.keytab. -
Build and start the application and test it:
curl --negotiate -u bob@EXAMPLE.COM -v http://localhost:8080/api/users/me
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
Type |
Default |
|
|---|---|---|
If the Kerberos extension is enabled. Environment variable: |
boolean |
|
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: |
boolean |
|
The container image name to use, for container based DevServices providers. See https://github.com/kerberos-io/kerberos-docker. Environment variable: |
string |
|
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 Container sharing is only used in dev mode. Environment variable: |
boolean |
|
The value of the Container sharing is only used in dev mode. Environment variable: |
string |
|
The JAVA_OPTS passed to the keycloak JVM Environment variable: |
string |
|
The Kerberos realm. Environment variable: |
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 Environment variable: |
string |
|
Specifies if a JAAS configuration 'debug' property should be enabled. Note this property is only effective when Environment variable: |
boolean |
|
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 Environment variable: |
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: |
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: |
string |
|
Service principal password. Set this property only if using Environment variable: |
string |
|
Specifies whether to use Spnego or Kerberos OID. Environment variable: |
boolean |
|
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: |
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
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 Environment variable: |
string |
|
Specifies if a JAAS configuration 'debug' property should be enabled. Note this property is only effective when Environment variable: |
boolean |
|
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 Environment variable: |
string |
|
User Principal name. Environment variable: |
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: |
string |
|
Service Principal name Environment variable: |
string |
|
User principal password. Set this property only if using Environment variable: |
string |
|
Specifies whether to use Spnego or Kerberos OID. Environment variable: |
boolean |
|