Java SPNEGO Authentication & Kerberos Constrained Delegation (KCD) to backend service
I have a Java web application which do SPNEGO authentication of clients in a Windows Active Directory environment. To authenticate the user we use code from the good old SPNEGO SourceForge project.
String encodedAuthToken = (String) credentials;
LOG.debug("Encoded auth token: " + encodedAuthToken);
byte[] authToken = B64Code.decode(encodedAuthToken);
GSSManager manager = GSSManager.getInstance();
try {
Oid krb5Oid = new Oid("1.3.6.1.5.5.2");
GSSName gssName = manager.createName(_targetName, null);
GSSCredential serverCreds = manager.createCredential(gssName, GSSCredential.INDEFINITE_LIFETIME, krb5Oid, GSSCredential.INITIATE_AND_ACCEPT);
GSSContext gContext = manager.createContext(serverCreds);
if (gContext != null) {
while (!gContext.isEstablished()) {
authToken = gContext.acceptSecContext(authToken, 0, authToken.length);
}
if (gContext.isEstablished()) {
// Login succeeded!
String clientName = gContext.getSrcName().toString();
}
}
}
The authentication works good but we also have a requirement to delegate the user credentials to a back-end service (Exchange EWS), using constrained delegation. When configuring this in our AD it looks like a small difference, but it's not. See: AD delegation settings
The difference is described here: msdn.microsoft.com/en-us/library/cc246080.aspx?f=255&MSPPError=-2147217396 With unconstrained delegation we could simply use the available delegated credentials when we call the back-end service and it would all be good:
GSSCredential delegatedCreds = gContext.getDelegCred()
SpnegoHttpURLConnection conn = new SpnegoHttpURLConnection(clientCreds);
With constrained delegation we have no access to the users TGT and it seems we need to use the MS-SFU (S4U2proxy) Kerberos extension which Java 8 is suppose to support. The only example I could find is this one: https://github.com/ymartin59/java-kerberos-sfudemo (thanks Yves Martin for that!)
Now to my problem... After my authentication I basically end up with the username of the authenticated user (see "clientName" in code above).
Do we really need to use the S4U2self mechanism to impersonate the user here? The client just sent us it's Kerberos Service Ticket (wrapped in the SPNEGO token I can't decode). Ideally we should be able to use that service ticket and my own service's TGT to authenticate the user (using the S4U2proxy mechanism)? But I do not understand how.
So now I'm wondering if it's possible to tie together our SPNEGO authentication with S4U2proxy delegation?
Many thanks for any input on this.
Solution 1:
I've actually been doing something like this recently but am using spring security kerberos. I put an example on github here. The key thing that I found that I needed set up to use constrained delegation like you want it and S4U2Proxy was to make sure (if you're using Oracle/OpenJDK) you set isInitiator=true
in your JAAS Config so that when getDelegCred is called you get back a Krb5ProxyCredential. See comment here. With that credential, you can use it to create service ticket tokens on the Users behalf for the services you are constrained to use in the normal fashion, like this.
Solution 2:
I've done a lot of investigation on Kerberos constrained delegation, and finally I've figured out the correct way of doing it using Java.
Settings on Domain Controller
1) No Delegation: Do not trust this account for delegation
You (service user) can not get delegated credentials of the user. It means you can not perform any task on end user's behalf. At the most you can do is to accept the incoming ticket from the user(usually browser) and get it verified by passing it to KDC. In response, KDC will tell you for which user(or principal) this ticket is issued to, but no credentials will be passed.
2) Unconstrained Delegation: Trust this account for delegation to any service (Kerberos only)
With this option, you (service user) get the delegated credentials of the user. Moreover, what you get is a TGT of the user. Using this TGT, you can request TGS (service ticket) on user's behalf for any service.
3) Trust this account for delegation to specified services (Kerberos only)
Here, you specify the services to which you can use the delegated credentials. It means when this option is enabled, you get the delegated credentials, however, you are allowed to use them only to get end user's TGS for the specified services.
Another important point is, you must have end user's TGS (end user's TGS for your web app). Then using this TGS, you can request KDC the end user's TGS for another service.
4) Trust this account for delegation to specified services (Any Protocol)
This is also known as protocol transition. In this option also, you need to specify the services for which you can request the TGS to KDC on user's behalf.
You (service user) are allowed to 'impersonate' the end user, without having any kind of ticket from the end user. You can impersonate any user, and get TGS for the specified services. This option is useful for backgroung processes or schedulars where end user interaction is not possible.
Java Code Samples
1) Getting Delegated Credentials (useful in option 2 and 3 stated above)
// ---------------------------------
// step 1: Login using service user credentials and get its TGT
// ---------------------------------
Subject subject = new Subject();
Krb5LoginModule krb5LoginModule = new Krb5LoginModule();
Map<String,String> optionMap = new HashMap<String,String>();
optionMap.put("keyTab", "c:\\ticket\\sapuser.keytab");
optionMap.put("principal", "HTTP/TEST"); // SPN you mapped to the service user while creating the keytab file
optionMap.put("doNotPrompt", "true");
optionMap.put("refreshKrb5Config", "true");
optionMap.put("useTicketCache", "true");
optionMap.put("renewTGT", "true");
optionMap.put("useKeyTab", "true");
optionMap.put("storeKey", "true");
optionMap.put("isInitiator", "true"); // needed for delegation
optionMap.put("debug", "true"); // trace will be printed on console
krb5LoginModule.initialize(subject, null, new HashMap<String,String>(), optionMap);
krb5LoginModule.login();
krb5LoginModule.commit();
// ---------------------------------
// Step 2: Use login context of this service user, accept the kerberos token (TGS) coming from end user
// ---------------------------------
public GSSCredential validateTicket(byte[] token) {
try {
return Subject.doAs(this.serviceSubject, new KerberosValidateAction(token));
}
catch (PrivilegedActionException e) {
throw new BadCredentialsException("Kerberos validation not successful", e);
}
}
private class KerberosValidateAction implements PrivilegedExceptionAction<GSSCredential> {
byte[] kerberosTicket;
public KerberosValidateAction(byte[] kerberosTicket) {
this.kerberosTicket = kerberosTicket;
}
@Override
public GSSCredential run() throws Exception {
byte[] responseToken = new byte[0];
GSSName gssName = null;
GSSContext context = GSSManager.getInstance().createContext((GSSCredential) null);
while (!context.isEstablished()) {
responseToken = context.acceptSecContext(kerberosTicket, 0, kerberosTicket.length);
gssName = context.getSrcName();
if (gssName == null) {
throw new BadCredentialsException("GSSContext name of the context initiator is null");
}
}
//check if the credentials can be delegated
if (!context.getCredDelegState()) {
SecurityLogger.getLogger().error("Credentials can not be delegated. Please make sure that delegation is enabled for the service user. This may cause failures while creating Kerberized application.");
return null;
}
// only accepts the delegated credentials from the calling peer
GSSCredential clientCred = context.getDelegCred(); // in case of Unconstrained Delegation, you get the end user's TGT, otherwise TGS only
return clientCred;
}
}
// ---------------------------------
// Step 3: Initiate TGS request for another service using delegated credentials obtained in previous step
// ---------------------------------
private Object getServiceTicket(GSSCredential clientCred) throws PrivilegedActionException {
Object o = Subject.doAs(new Subject(), (PrivilegedExceptionAction<Object>) () -> {
GSSManager manager = GSSManager.getInstance();
Oid SPNEGO_OID = new Oid("1.3.6.1.5.5.2");
Oid KRB5_PRINCIPAL_OID = new Oid("1.2.840.113554.1.2.2.1");
GSSName servicePrincipal = manager.createName("HTTP/TEST", KRB5_PRINCIPAL_OID); // service to which the service user is allowed to delegate credentials
ExtendedGSSContext extendedContext = (ExtendedGSSContext) manager.createContext(servicePrincipal, SPNEGO_OID, clientCred, GSSContext.DEFAULT_LIFETIME);
extendedContext.requestCredDeleg(true);
byte[] token = new byte[0];
token = extendedContext.initSecContext(token, 0, token.length); // this token is the end user's TGS for "HTTP/TEST" service, you can pass this to the actual HTTP/TEST service endpoint in "Authorization" header.
return token;
});
return o;
}
2) Getting Impersonated Credentials (useful in option 4 stated above)
Initial steps are similar as mentioned inside step 1 above. You need to login using service user credentials. There is small change in 'run' method, which is given below:
@Override
public GSSCredential run() throws Exception {
GSSName gssName = null;
GSSManager manager = GSSManager.getInstance();
GSSCredential serviceCredentials = manager.createCredential(GSSCredential.INITIATE_ONLY);
GSSName other = manager.createName("bhushan", GSSName.NT_USER_NAME, kerberosOid); // any existing user
GSSCredential impersonatedCredentials = ((ExtendedGSSCredential) serviceCredentials).impersonate(other);
return impersonatedCredentials;
}
}
You can see that we don't need user's TGS in this case.
Getting TGS on user's behalf for other service, is same as mentioned in step 3 given in above code. Just pass these impersonatedCredentials instead of delegatedCredentials.
I hope this will be helpful.
Thanks,
Bhushan