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.COM
or add a new realm, example,QUARKUS.COM
. Make sure the realm’skdc
andadmin_server
properties point tolocalhost
. -
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
andsystemctl 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 itsapplication.properties
to 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 |
|