Firstly, I checked the web services framework we were using and found that there was no Kerberos support. The best answer I got from the Axis 2 community was "No, this is not supported and we don't know of anyone doing it in Axis 2". So this meant doing everything from the ground on up. The first step was to work out how the heck Kerberos worked, and how to authenticate with it in Java.
The first place people end up when on such a quest is usually the Sun tutorial on JAAS and GSS. While this tutorial kind've explains the concepts and how to do it, it is hard to get a complete picture of what you're doing. Secondly, the code is a socket based client/server which is not useful at all, as only a lunatic would be writing their own server communications layer in these days of NIO and SOA. The second place is the Sun Java forum for Kerberos, which has a couple of smart people who answer a lot of questions, but I found their responses more often than not ended up confusing me.
This article is my take on a Kerberos "Hello World" client/server in a more applicable way to modern systems. I changed the concept slightly to suit my needs - that is, it needed to be the foundation for a SOAP inter-operable system. Instead of using socket communications, my code base-64 encodes the Kerberos service ticket and writes it to a file. Subsequently, when the server is run, it opens that file, reads the base-64 string and decodes it to a service ticket which is then used to initiate the Kerberos context/session. When sending Kerberos tickets over SOAP they are base-64 encoded, hence my implementation. Once the security context has been initialised, the server determines the client who made the request and prints "Hello World
An intended 'feature' of my blogs is that I will include fully working projects and code with them, that should run straight out of the box. Below you can find a downloadable source distribution of my NetBeans project, that includes compiled classes, and .bat files for running the client and server. All that needs to be done before running, is to re-configure the properties files to work with your domain/KDC.
The code is fairly straight-forward, and I've commented it heavily. Points to note for newbies are:
1. JAAS is used to login at both the client and server sides. Once you have successfully logged in via JAAS, you get a subject which contains all your secret keys that the KDC gave you after logging in. These keys are used to encrypt and decrypt tickets that are only meant for you. Only you and the KDC know your secret keys, hence only you can decrypt tokens meant for you.
2. When the JAAS login is performed in the client, the ticket that is retrieved (and placed into your subject) is the TGT (ticket granting ticket). This is required to then subsequently request a service ticket for accessing a service. Additionally, when the server logs in, it gets its own TGT and a set of keys which are used to decode the incoming service ticket.
3. The call to initSecContext() in the Client is the act of requesting a service ticket. The raw byte array that this returns is the service ticket. This contains the client principal user name and a session key that only the client, server and KDC know about. The ticket is encrypted by the KDC so that only the server can decode it.
4. The security context initation and acceptance are privileged actions. This means that they can only be performed by the identity with the correct credentials/keys to do so. In the examples, the code subject.doAs() is how this is implemented. The subject that is passed in as a parameter is used to decode whatever token is being accepted or used.
5. The code:
krb5Oid = new Oid( "1.2.840.113554.1.2.2");instructs the Java APIs that the security mechanism you are wanting to use is Kerberos version 5.
6. In my examples, requestMutualAuth is always turned off, so that there is only one token sent from client to server to initiate and accept a security context. This is different from the Sun tutorial in that many passes can be made. In web service security, it is a big hassle if more than one pass is required to initiate the context, as the SOAP message may be asynchronous or atomic.
7. The GSS API that you will often see mentioned is the best means of doing Kerberos authentication in Java. GSS stands for Generic Security Services API. It provides a generic way of performing security/authentication services, that does not tie you down to a specific implementation (e.g. Kerberos). The benefit of using it here, other than being able to use a different security mechanism is that it makes interoperability with .NET extremely simple because .NET also uses GSS. A GSS token (service ticket) generated in Java, can be easily understood by a .NET program, and vice-versa. Web service Kerberos security uses GSS tokens when initiating secured sessions. Note that it is possible to use the actual Kerberos libraries in Java to do authentication, but there is no benefit at all to doing so.
Configuring the example project
Before running the project, it needs to be configured to use your domain and KDC correctly. The settings in the included config files are specific to running on my laptop against Apache Directory Server. I have also tested it against a Windows 2003 domain running Active Directory.
Firstly, I am assuming that your KDC has been setup correctly. That is, you have a user account, a server account and the latter has a Kerberos service principal setup (see my previous blog on how to do this in Active Directory).
The file jaas.conf has to be changed to put the correct service principal in the Server configuration section. This is the Kerberos principal that your service account is mapped to. My example is set to principal="webserver/bully@EXAMPLE.COM"; bully is my laptop computer name, and webserver is the name of the service account.
The client.properties file needs to be changed to include your realm name (which HAS to be capitalised), the ip address of your KDC, your client account login details and the service account name. The service account name gets converted into a Kerberos principal by the Java libraries, so you don't need to specify it as a Kerberos name here.
The server.properties file needs to be changed to specify your KDC, your realm and the password of your service account.
You can now run client.bat and then server.bat and see if your Kerberos Hello World application is working. Note that you should be running with at least Java 1.5.07 and it needs to be the Sun JVM. Older JVMs don't support all the encryption types that Kerberos v5 uses, and the Sun JVM specific base-64 libraries are used in my demo.
Error Messages
Identifier doesn't match expected value (906) - this is because your password is wrong, so you couldn't be authenticated against the KDC.
Server not found in Kerberos database - this is either because the service account doesn't exist in your database OR the Kerberos service principal name wasn't recognised by your KDC. Try another SPN mapping or modify the config file to have a different service principal name. If you are using Apache Directory, you can check the server logs for what SPN is actually getting passed through to the KDC.
Integrity check on decrypted field failed - this generally means that (a) your password failed during the JAAS login to the KDC, or (b) the ticket you are trying to decode couldn't be decrypted. This is because the ticket is not meant for your server, so it doesn't have the key for decrypting it. In this case, change the service principal that your server is logging in as to resolve this.
Encryption type not supported by KDC - this is generally due to using Active Directory on a Windows 2003 domain. This requires setting a registry entry on both your client and domain machines, allowTGTSessionKey. See the section in the following Sun document for the complete guide to doing this. Note that I also have had this error while using Apache Directory server, which was due to my JDK 1.5 not being past version 7 (or thereabouts).
Download the NetBeans project and source/binaries here. |
![]() | Client.java |
package javamonkey.app.gss;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.security.PrivilegedAction;
import java.util.Properties;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.ietf.jgss.Oid;
import sun.misc.BASE64Encoder;
/**
* <p>Client logs in to a Key Distribution Center (KDC) using JAAS and then
* requests a service ticket for the server, base 64 encodes it and writes it
* to the file <i>service-ticket.txt</i>.</p>
* <p>This class, in combination with the <i>Server</i> class illustrates the
* use of the JAAS and GSS APIs for initiating a security context using the
* Kerberos protocol.</p>
* <p>This requires a KDC/domain controller such as Active Directory or Apache
* Directory. The KDC configuration details are stored in the
* <i>client.properties</i> file, while the JAAS details are stored in the
* file <i>jaas.conf</i>.</p>
* @author Ants
*/
public class Client {
public static void main( String[] args) {
try {
// Setup up the Kerberos properties.
Properties props = new Properties();
props.load( new FileInputStream( "client.properties"));
System.setProperty( "sun.security.krb5.debug", "true");
System.setProperty( "java.security.krb5.realm", props.getProperty( "realm"));
System.setProperty( "java.security.krb5.kdc", props.getProperty( "kdc"));
System.setProperty( "java.security.auth.login.config", "./jaas.conf");
System.setProperty( "javax.security.auth.useSubjectCredsOnly", "true");
String username = props.getProperty( "client.principal.name");
String password = props.getProperty( "client.password");
// Oid mechanism = use Kerberos V5 as the security mechanism.
krb5Oid = new Oid( "1.2.840.113554.1.2.2");
Client client = new Client();
// Login to the KDC.
client.login( username, password);
// Request the service ticket.
client.initiateSecurityContext( props.getProperty( "service.principal.name"));
// Write the ticket to disk for the server to read.
encodeAndWriteTicketToDisk( client.serviceTicket, "./security.token");
System.out.println( "Service ticket encoded to disk successfully");
}
catch ( LoginException e) {
e.printStackTrace();
System.err.println( "There was an error during the JAAS login");
System.exit( -1);
}
catch ( GSSException e) {
e.printStackTrace();
System.err.println( "There was an error during the security context initiation");
System.exit( -1);
}
catch ( IOException e) {
e.printStackTrace();
System.err.println( "There was an IO error");
System.exit( -1);
}
}
public Client() {
super();
}
private static Oid krb5Oid;
private Subject subject;
private byte[] serviceTicket;
// Authenticate against the KDC using JAAS.
private void login( String username, String password) throws LoginException {
LoginContext loginCtx = null;
// "Client" references the JAAS configuration in the jaas.conf file.
loginCtx = new LoginContext( "Client",
new LoginCallbackHandler( username, password));
loginCtx.login();
this.subject = loginCtx.getSubject();
}
// Begin the initiation of a security context with the target service.
private void initiateSecurityContext( String servicePrincipalName)
throws GSSException {
GSSManager manager = GSSManager.getInstance();
GSSName serverName = manager.createName( servicePrincipalName,
GSSName.NT_HOSTBASED_SERVICE);
final GSSContext context = manager.createContext( serverName, krb5Oid, null,
GSSContext.DEFAULT_LIFETIME);
// The GSS context initiation has to be performed as a privileged action.
this.serviceTicket = Subject.doAs( subject, new PrivilegedAction<byte[]>() {
public byte[] run() {
try {
byte[] token = new byte[0];
// This is a one pass context initialisation.
context.requestMutualAuth( false);
context.requestCredDeleg( false);
return context.initSecContext( token, 0, token.length);
}
catch ( GSSException e) {
e.printStackTrace();
return null;
}
}
});
}
// Base64 encode the raw ticket and write it to the given file.
private static void encodeAndWriteTicketToDisk( byte[] ticket, String filepath)
throws IOException {
BASE64Encoder encoder = new BASE64Encoder();
FileWriter writer = new FileWriter( new File( filepath));
String encodedToken = encoder.encode( ticket);
writer.write( encodedToken);
writer.close();
}
}
![]() | LoginCallbackHandler.java |
package javamonkey.app.gss;
import java.io.IOException;
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;
/**
* Password callback handler for resolving password/usernames for a JAAS login.
* @author Ants
*/
public class LoginCallbackHandler implements CallbackHandler {
public LoginCallbackHandler() {
super();
}
public LoginCallbackHandler( String name, String password) {
super();
this.username = name;
this.password = password;
}
public LoginCallbackHandler( String password) {
super();
this.password = password;
}
private String password;
private String username;
/**
* Handles the callbacks, and sets the user/password detail.
* @param callbacks the callbacks to handle
* @throws IOException if an input or output error occurs.
*/
public void handle( Callback[] callbacks)
throws IOException, UnsupportedCallbackException {
for ( int i=0; i<callbacks.length; i++) {
if ( callbacks[i] instanceof NameCallback && username != null) {
NameCallback nc = (NameCallback) callbacks[i];
nc.setName( username);
}
else if ( callbacks[i] instanceof PasswordCallback) {
PasswordCallback pc = (PasswordCallback) callbacks[i];
pc.setPassword( password.toCharArray());
}
else {
/*throw new UnsupportedCallbackException(
callbacks[i], "Unrecognized Callback");*/
}
}
}
}
![]() | Server.java |
package javamonkey.app.gss;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.security.PrivilegedAction;
import java.util.Properties;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.ietf.jgss.Oid;
import sun.misc.BASE64Decoder;
/**
* <p>Server logs in to a Key Distribution Center (KDC) using JAAS and then
* reads the encoded service ticket from the file <i>service-ticket.txt</i>.
* This ticket is decoded into a byte array and the security context is
* attempted to be accepted, using the GSS API.</p>
* <p>This class, in combination with the <i>Client</i> class illustrates the
* use of the JAAS and GSS APIs for initiating a security context using the
* Kerberos protocol.</p>
* <p>This requires a KDC/domain controller such as Active Directory or Apache
* Directory. The KDC configuration details are stored in the
* <i>server.properties</i> file, while the JAAS details are stored in the
* file <i>jaas.conf</i>.</p>
* @author Ants
*/
public class Server {
public static void main( String[] args) {
try {
// Setup up the Kerberos properties.
Properties props = new Properties();
props.load( new FileInputStream( "server.properties"));
System.setProperty( "sun.security.krb5.debug", "true");
System.setProperty( "java.security.krb5.realm", props.getProperty( "realm"));
System.setProperty( "java.security.krb5.kdc", props.getProperty( "kdc"));
System.setProperty( "java.security.auth.login.config", "./jaas.conf");
System.setProperty( "javax.security.auth.useSubjectCredsOnly", "true");
String password = props.getProperty( "service.password");
// Oid mechanism = use Kerberos V5 as the security mechanism.
krb5Oid = new Oid( "1.2.840.113554.1.2.2");
Server server = new Server();
// Login to the KDC.
server.login( password);
byte serviceTicket[] = loadTokenFromDisk();
// Request the service ticket.
String clientName = server.acceptSecurityContext( serviceTicket);
System.out.println( "\nSecurity context successfully initialised!");
System.out.println( "\nHello World " + clientName + "!");
}
catch ( LoginException e) {
e.printStackTrace();
System.err.println( "There was an error during the JAAS login");
System.exit( -1);
}
catch ( GSSException e) {
e.printStackTrace();
System.err.println( "There was an error during the security context acceptance");
System.exit( -1);
}
catch ( IOException e) {
e.printStackTrace();
System.err.println( "There was an IO error");
System.exit( -1);
}
}
// Load the security token from disk and decode it. Return the raw GSS token.
private static byte[] loadTokenFromDisk() throws IOException {
BufferedReader in = new BufferedReader( new FileReader( "security.token"));
System.out.println( new File( "security.token").getAbsolutePath());
String str;
StringBuffer buffer = new StringBuffer();
while ((str = in.readLine()) != null) {
buffer.append( str + "\n");
}
in.close();
//System.out.println( buffer.toString());
BASE64Decoder decoder = new BASE64Decoder();
return decoder.decodeBuffer( buffer.toString());
}
private static Oid krb5Oid;
private Subject subject;
// Authenticate against the KDC using JAAS.
private void login( String password) throws LoginException {
LoginContext loginCtx = null;
// "Client" references the JAAS configuration in the jaas.conf file.
loginCtx = new LoginContext( "Server",
new LoginCallbackHandler( password));
loginCtx.login();
this.subject = loginCtx.getSubject();
}
// Completes the security context initialisation and returns the client name.
private String acceptSecurityContext( final byte[] serviceTicket)
throws GSSException {
krb5Oid = new Oid( "1.2.840.113554.1.2.2");
// Accept the context and return the client principal name.
return Subject.doAs( subject, new PrivilegedAction<String>() {
public String run() {
try {
// Identify the server that communications are being made to.
GSSManager manager = GSSManager.getInstance();
GSSContext context = manager.createContext( (GSSCredential) null);
context.acceptSecContext( serviceTicket, 0, serviceTicket.length);
return context.getSrcName().toString();
}
catch ( Exception e) {
e.printStackTrace();
return null;
}
}
});
}
}
![]() | jaas.conf |
Client {
com.sun.security.auth.module.Krb5LoginModule required
useTicketCache=false;
};
Server {
com.sun.security.auth.module.Krb5LoginModule required
useKeyTab=false
storeKey=true
useTicketCache=false
principal="webserver/bully@EXAMPLE.COM";
};
![]() | client.properties |
realm=EXAMPLE.COM
kdc=127.0.0.1
client.principal.name=anthony
client.password=Password99
service.principal.name=webserver
![]() | server.properties |
realm=EXAMPLE.COM
kdc=localhost
service.password=Password99
Download the NetBeans project and source/binaries here.
15 comments:
Thanks for great blog posts on Kerberos and the Java side of it.
Could you please give some input of how you setup the Apache Directory Server, i.e. what version, what you needed to configure, an ldif file for creating a user and a service in the directory and so on.
Best regards
Jimmy
Hi Jimmy, I've posted a new blog on using Apache Directory Server as a KDC - http://thejavamonkey.blogspot.com/2008/07/using-apache-directory-server-as-kdc.html
Have a look and let me know what you think.
Thanks mate, you are a legend, The explanation helped me clear all my doubts. Appreciate the effort
Thank you very much! It looks really good and will help a lot of people.
Small note:
Client.java uses username "anthony" (see client.properties), but the LDIF file (in other blog) contains user named "monkey" ;-)
Thanks you!
It helps me a lot and ends waste of time searching something clear on Kerberos java client.
Damned Google!
Thanks for such a detailed tutorial. Helped me to clear some facts regarding using Kerberos with web services
Thank you for the wonderful hands-on tutorial on Kerberos.
I am getting 'Server not found in the Kerberos Database (7) error.
Have a basic doubt - My machine name is GC57DG1-SEC.r1-core.r1.xyz.net - with r1-core.r1.xyz.net is the domain name.
In the JAAS.conf - the server principal="webserver/GC57DG1-SEC.r1-core.r1.xyz.net@EXAMPLE.COM";
Is there a way for me to check whether this is correct? For some reasons, I am not able to run the DS in debug mode. Have been banging my head over this :-) Would greatly appreciate any help.
Thanks
It worked for me once I changed the Oid to GSSName.NT_USER_NAME instead of NT_HOSTBASED_SERVICE.
Thanks
Thank you so much for this sample code. I was having a hard time understanding the Sun tutorial and this helped make things clearer. Honestly, the hardest part was the Active Directory stuff to make sure I mapped a user to an SPN properly.
Thanks again.
I have the client work. However, when I run the server, I got the following exception. Thank you for your help.
Entered Krb5Context.acceptSecContext with state=STATE_NEW
>>> EType: sun.security.krb5.internal.crypto.ArcFourHmacEType
Checksum failed !
GSSException: Failure unspecified at GSS-API level (Mechanism level: Checksum failed)
at sun.security.jgss.krb5.Krb5Context.acceptSecContext(Unknown Source)
at sun.security.jgss.GSSContextImpl.acceptSecContext(Unknown Source)
at sun.security.jgss.GSSContextImpl.acceptSecContext(Unknown Source)
at javamonkey.app.gss.Server$1.run(Server.java:118)
at javamonkey.app.gss.Server$1.run(Server.java:113)
at java.security.AccessController.doPrivileged(Native Method)
at javax.security.auth.Subject.doAs(Unknown Source)
at javamonkey.app.gss.Server.acceptSecurityContext(Server.java:112)
at javamonkey.app.gss.Server.main(Server.java:56)
Here is SPN in the client.properties:
service.principal.name=HOST/BCIDCVM01.bci.sas.com
Hey, there are two things you should try:
1. If you are using a version of the JDK that is less than 1.5.0_07 then, update to a newer version of Java.
2. On the Active Directory accounts on your domain server, make sure you check the box "use DES encryption type" for the client and service accounts.
That solved my problem. Thank you very much.
Thanks for the example monkey boy, it was a great starting point. I did run into some problems, however. Here's how I resolved them ...
When starting server you may get error "Client not found in Kerberos database." Here's why ...
When server "logs in" via JAAS, it attempts to retrieve a TGT and uses value of jaas 'principal' attribute as user name (along w/ password supplied by callback.) Because this value represents the SPN and not the client name, the login fails.
Server should be passive, i.e. no TGT required. The fix is add isInitiator=false to jaas.conf 'Server' app entry. Jaas login then simply creates a Kerberos principal (using SPN) and Kerberos key using password supplied by callback. No UDP message to AD required.
When initiating security context within Client, you may get error "Server not found in Kerberos database." Here's why ...
The service principal name is "assembled" via the GSS api and MUST exactly match value registered within KDC. To fix:
1. assuming SPN mapped to service name on KDC is
'myservice/my.host.name'
2. service.principal.name in Client.properties must match the above name exactly - DO NOT SURROUND WITH QUOTES
3. you must append domain name to principal attribute in Server entry in jaas.conf (THE DOMAIN NAME MUST BE ALL CAPS and value MUST BE SURROUNDED BY QUOTES), e.g.
principal="myservice/my.host.name@MY.DOMAIN"
4. Modify example code which assembles the SPN to use Kerberos formatted name:
Oid krb5PrincipalNameType = new Oid("1.2.840.113554.1.2.2.1");
GSSName serverName = manager.createName(servicePrincipalName, krb5PrincipalNameType);
Hope this helps someone.
Thanks for a good example.
Still wondering, is there some way to use the logged on Windows users Kerberos with these newer Windows versions without having to set this "allowTGTSessionKey" cause I don't feel so comfortable with messing with clients registry settings...
Post a Comment