Almost all our Web Services are secured with
SPNEGO. This way we can forward the identity of the user calling a service to another service, or/and through the tiers of the service. If, for example, a user calls the “check my mail” WS, the service can get the identity of the user from the
SPNEGO token, request a new kerberos token from the
KDC for that user, and use that token to check for new mail on the
IMAP server on behalf of the user.
Client[1] ---> WS[2] ---> more services[3]
Client calls WS, authenticates with his kerberos token, and WS can use that token to authenticate to more services on behalf of the Client.
I had to search the Web for a few hours to find out how
SPNEGO and delegation works with Java, so here is a summary:
- The client sends a request to the server
- The server answers with
Status: 401 - Authorization Required
WWW-Authenticate: Negotiate
- The client receives the 401 and sends the SPNEGO token to the server:
Authorization=Negotiate YIIGHwYGKwYBBQUCoIIGE[...]
- The server then has to decode and validate that token. If the token is forwardable then the server can use it to request a new token to authenticate to other services.
This is how to decode a
SPNEGO token in Java:
byte[] token = null;
byte[] tokenForPeer = null;
byte[] tokenForEndpoint = new byte[0];
String endpointSPN = null;
GSSManager manager = GSSManager.getInstance();
GSSContext context = null;
GSSCredential clientCred = null;
GSSCredential myCred = null;
try {
//Oid krb5MechOid = new Oid("1.2.840.113554.1.2.2");
Oid spnegoMechOid = new Oid("1.3.6.1.5.5.2");
//first obtain it's own credentials...
myCred = manager.createCredential(null, GSSCredential.DEFAULT_LIFETIME, spnegoMechOid, GSSCredential.ACCEPT_ONLY);
//...and create a context for this credentials...
context = manager.createContext(myCred);
//...then use that context to authenticate the calling peer by reading his
//spnego token
System.out.println(authorization);
token = Base64.decode(authorization);
tokenForPeer = context.acceptSecContext(token, 0, token.length);
if (!context.isEstablished()) return false;
if (tokenForPeer != null) {
System.out.println("there is a token to send back to the peer, but I leave this out for now");
}
System.out.println("Context Established! ");
System.out.println("Client principal is " + context.getSrcName());
System.out.println("Server principal is " + context.getTargName());
} catch (WSSecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (GSSException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
the authorization String contains the
SPNEGO token from the request. Once we have established the context, we can check if the credentials can be delegated, and then request the new token:
//check if the credentials can be delegated
if (!context.getCredDelegState()) {
System.out.println("credentials can not be delegated!");
return false;
}
//get the delegated credentials from the calling peer...
clientCred = context.getDelegCred();
//now create the spnego token to send to the endpoint:
//create target server SPN
endpointSPN = "HTTP/spnegotestserver.domain.com@REALM.COM";
System.out.println("Endpoint: " + endpointSPN);
GSSName gssServerName = manager.createName(endpointSPN, GSSName.NT_USER_NAME);
//...and create a new context pretending to be the caller
clientContext = manager.createContext(gssServerName.canonicalize(spnegoMechOid), spnegoMechOid, clientCred, GSSContext.DEFAULT_LIFETIME);
//this should be an option: enable GSS credential delegation
clientContext.requestCredDeleg(true);
// create a SPNEGO token for the target server
tokenForEndpoint = clientContext.initSecContext(tokenForEndpoint, 0, tokenForEndpoint.length);
Done. Now we have a new tokenForEndpoint object that contains a valid
SPNEGO token with the delegated credentials from the calling user. Insert it in the headers for the
HTTP request to the next service in the chain like this
"Negotiate " + Base64.encode(tokenForEndpoint)
and you are good to go.