KEYCLOAK-1928 Kerberos working with IBM JDK

This commit is contained in:
mposolda 2016-02-24 22:15:04 +01:00
parent 13acd512b6
commit 7f32ce810a
11 changed files with 344 additions and 142 deletions

View file

@ -17,6 +17,9 @@
package org.keycloak.common.constants; package org.keycloak.common.constants;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.Oid;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
@ -31,19 +34,33 @@ public class KerberosConstants {
/** /**
* OID of SPNEGO mechanism. See http://www.oid-info.com/get/1.3.6.1.5.5.2 * OID of SPNEGO mechanism. See http://www.oid-info.com/get/1.3.6.1.5.5.2
*/ */
public static final String SPNEGO_OID = "1.3.6.1.5.5.2"; private static final String SPNEGO_OID_STR = "1.3.6.1.5.5.2";
public static final Oid SPNEGO_OID;
/** /**
* OID of Kerberos v5 mechanism. See http://www.oid-info.com/get/1.2.840.113554.1.2.2 * OID of Kerberos v5 mechanism. See http://www.oid-info.com/get/1.2.840.113554.1.2.2
*/ */
public static final String KRB5_OID = "1.2.840.113554.1.2.2"; private static final String KRB5_OID_STR = "1.2.840.113554.1.2.2";
public static final Oid KRB5_OID;
/** /**
* OID of Kerberos v5 name. See http://www.oid-info.com/get/1.2.840.113554.1.2.2.1 * OID of Kerberos v5 name. See http://www.oid-info.com/get/1.2.840.113554.1.2.2.1
*/ */
public static final String KRB5_NAME_OID = "1.2.840.113554.1.2.2.1"; private static final String KRB5_NAME_OID_STR = "1.2.840.113554.1.2.2.1";
public static final Oid KRB5_NAME_OID;
static {
try {
KRB5_OID = new Oid(KerberosConstants.KRB5_OID_STR);
KRB5_NAME_OID = new Oid(KerberosConstants.KRB5_NAME_OID_STR);
SPNEGO_OID = new Oid(KerberosConstants.SPNEGO_OID_STR);
} catch (GSSException e) {
throw new RuntimeException(e);
}
}
/** /**

View file

@ -0,0 +1,227 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.common.util;
import java.io.File;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.security.PrivilegedExceptionAction;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.security.auth.Subject;
import javax.security.auth.kerberos.KerberosPrincipal;
import javax.security.auth.kerberos.KerberosTicket;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.Configuration;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.keycloak.common.constants.KerberosConstants;
/**
* Provides abstraction to handle differences between various JDK vendors (Sun, IBM)
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public abstract class KerberosJdkProvider {
public abstract Configuration createJaasConfigurationForServer(String keytab, String serverPrincipal, boolean debug);
public abstract Configuration createJaasConfigurationForUsernamePasswordLogin(boolean debug);
public abstract KerberosTicket gssCredentialToKerberosTicket(KerberosTicket kerberosTicket, GSSCredential gssCredential);
public GSSCredential kerberosTicketToGSSCredential(KerberosTicket kerberosTicket) {
return kerberosTicketToGSSCredential(kerberosTicket, GSSCredential.DEFAULT_LIFETIME, GSSCredential.INITIATE_AND_ACCEPT);
}
// Actually same on both JDKs
public GSSCredential kerberosTicketToGSSCredential(KerberosTicket kerberosTicket, final int lifetime, final int usage) {
try {
final GSSManager gssManager = GSSManager.getInstance();
KerberosPrincipal kerberosPrincipal = kerberosTicket.getClient();
String krbPrincipalName = kerberosTicket.getClient().getName();
final GSSName gssName = gssManager.createName(krbPrincipalName, KerberosConstants.KRB5_NAME_OID);
Set<KerberosPrincipal> principals = Collections.singleton(kerberosPrincipal);
Set<GSSName> publicCreds = Collections.singleton(gssName);
Set<KerberosTicket> privateCreds = Collections.singleton(kerberosTicket);
Subject subject = new Subject(false, principals, publicCreds, privateCreds);
return Subject.doAs(subject, new PrivilegedExceptionAction<GSSCredential>() {
@Override
public GSSCredential run() throws Exception {
return gssManager.createCredential(gssName, lifetime, KerberosConstants.KRB5_OID, usage);
}
});
} catch (Exception e) {
throw new KerberosSerializationUtils.KerberosSerializationException("Unexpected exception during convert KerberosTicket to GSSCredential", e);
}
}
public static KerberosJdkProvider getProvider() {
if (KerberosSerializationUtils.JAVA_INFO.contains("IBM")) {
return new IBMJDKProvider();
} else {
return new SunJDKProvider();
}
}
// IMPL Subclasses
// Works for Oracle and OpenJDK
private static class SunJDKProvider extends KerberosJdkProvider {
@Override
public Configuration createJaasConfigurationForServer(final String keytab, final String serverPrincipal, final boolean debug) {
return new Configuration() {
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
Map<String, Object> options = new HashMap<>();
options.put("storeKey", "true");
options.put("doNotPrompt", "true");
options.put("isInitiator", "false");
options.put("useKeyTab", "true");
options.put("keyTab", keytab);
options.put("principal", serverPrincipal);
options.put("debug", String.valueOf(debug));
AppConfigurationEntry kerberosLMConfiguration = new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options);
return new AppConfigurationEntry[] { kerberosLMConfiguration };
}
};
}
@Override
public Configuration createJaasConfigurationForUsernamePasswordLogin(final boolean debug) {
return new Configuration() {
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
Map<String, Object> options = new HashMap<>();
options.put("storeKey", "true");
options.put("debug", String.valueOf(debug));
AppConfigurationEntry kerberosLMConfiguration = new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options);
return new AppConfigurationEntry[] { kerberosLMConfiguration };
}
};
}
// Note: input kerberosTicket is null for Sun based JDKs
@Override
public KerberosTicket gssCredentialToKerberosTicket(KerberosTicket kerberosTicket, GSSCredential gssCredential) {
try {
Class<?> gssUtil = Class.forName("com.sun.security.jgss.GSSUtil");
Method createSubject = gssUtil.getMethod("createSubject", GSSName.class, GSSCredential.class);
Subject subject = (Subject) createSubject.invoke(null, null, gssCredential);
Set<KerberosTicket> kerberosTickets = subject.getPrivateCredentials(KerberosTicket.class);
Iterator<KerberosTicket> iterator = kerberosTickets.iterator();
if (iterator.hasNext()) {
return iterator.next();
} else {
throw new KerberosSerializationUtils.KerberosSerializationException("Not available kerberosTicket in subject credentials. Subject was: " + subject.toString());
}
} catch (KerberosSerializationUtils.KerberosSerializationException ke) {
throw ke;
} catch (Exception e) {
throw new KerberosSerializationUtils.KerberosSerializationException("Unexpected error during convert GSSCredential to KerberosTicket", e);
}
}
}
// Works for IBM JDK
private static class IBMJDKProvider extends KerberosJdkProvider {
@Override
public Configuration createJaasConfigurationForServer(String keytab, final String serverPrincipal, final boolean debug) {
final String keytabUrl = getKeytabURL(keytab);
return new Configuration() {
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
Map<String, Object> options = new HashMap<>();
options.put("noAddress", "true");
options.put("credsType","acceptor");
options.put("useKeytab", keytabUrl);
options.put("principal", serverPrincipal);
options.put("debug", String.valueOf(debug));
AppConfigurationEntry kerberosLMConfiguration = new AppConfigurationEntry("com.ibm.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options);
return new AppConfigurationEntry[] { kerberosLMConfiguration };
}
};
}
private String getKeytabURL(String keytab) {
try {
return new File(keytab).toURI().toURL().toString();
} catch (MalformedURLException mfe) {
System.err.println("Invalid keytab location specified in configuration: " + keytab);
mfe.printStackTrace();
return keytab;
}
}
@Override
public Configuration createJaasConfigurationForUsernamePasswordLogin(final boolean debug) {
return new Configuration() {
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
Map<String, Object> options = new HashMap<>();
options.put("credsType","initiator");
options.put("noAddress", "true");
options.put("debug", String.valueOf(debug));
AppConfigurationEntry kerberosLMConfiguration = new AppConfigurationEntry("com.ibm.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options);
return new AppConfigurationEntry[] { kerberosLMConfiguration };
}
};
}
// For IBM, kerberosTicket was set on JAAS Subject, so we can just return it
@Override
public KerberosTicket gssCredentialToKerberosTicket(KerberosTicket kerberosTicket, GSSCredential gssCredential) {
if (kerberosTicket == null) {
throw new KerberosSerializationUtils.KerberosSerializationException("Not available kerberosTicket in subject credentials in IBM JDK");
} else {
return kerberosTicket;
}
}
}
}

View file

@ -25,22 +25,13 @@ import java.io.ObjectInputStream;
import java.io.ObjectOutput; import java.io.ObjectOutput;
import java.io.ObjectOutputStream; import java.io.ObjectOutputStream;
import java.io.Serializable; import java.io.Serializable;
import java.lang.reflect.Method; import java.util.Iterator;
import java.util.Set;
import javax.security.auth.Subject;
import javax.security.auth.kerberos.KerberosTicket; import javax.security.auth.kerberos.KerberosTicket;
import org.ietf.jgss.GSSCredential; import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.Oid;
import org.keycloak.common.constants.KerberosConstants;
import org.keycloak.common.util.reflections.Reflections;
import sun.security.jgss.GSSCredentialImpl;
import sun.security.jgss.GSSManagerImpl;
import sun.security.jgss.krb5.Krb5InitCredential;
import sun.security.jgss.krb5.Krb5NameElement;
import sun.security.jgss.spi.GSSCredentialSpi;
import sun.security.krb5.Credentials;
/** /**
* Provides serialization/deserialization of kerberos {@link org.ietf.jgss.GSSCredential}, so it can be transmitted from auth-server to the application * Provides serialization/deserialization of kerberos {@link org.ietf.jgss.GSSCredential}, so it can be transmitted from auth-server to the application
@ -50,18 +41,9 @@ import sun.security.krb5.Credentials;
*/ */
public class KerberosSerializationUtils { public class KerberosSerializationUtils {
public static final Oid KRB5_OID;
public static final Oid KRB5_NAME_OID;
public static final String JAVA_INFO; public static final String JAVA_INFO;
static { static {
try {
KRB5_OID = new Oid(KerberosConstants.KRB5_OID);
KRB5_NAME_OID = new Oid(KerberosConstants.KRB5_NAME_OID);
} catch (GSSException e) {
throw new RuntimeException(e);
}
String javaVersion = System.getProperty("java.version"); String javaVersion = System.getProperty("java.version");
String javaRuntimeVersion = System.getProperty("java.runtime.version"); String javaRuntimeVersion = System.getProperty("java.runtime.version");
String javaVendor = System.getProperty("java.vendor"); String javaVendor = System.getProperty("java.vendor");
@ -72,50 +54,17 @@ public class KerberosSerializationUtils {
private KerberosSerializationUtils() { private KerberosSerializationUtils() {
} }
public static String serializeCredential(GSSCredential gssCredential) throws KerberosSerializationException { public static String serializeCredential(KerberosTicket kerberosTicket, GSSCredential gssCredential) throws KerberosSerializationException {
try { try {
if (gssCredential == null) { if (gssCredential == null) {
throw new KerberosSerializationException("Null credential given as input"); throw new KerberosSerializationException("Null credential given as input");
} }
if (!(gssCredential instanceof GSSCredentialImpl)) { kerberosTicket = KerberosJdkProvider.getProvider().gssCredentialToKerberosTicket(kerberosTicket, gssCredential);
throw new KerberosSerializationException("Unknown credential type: " + gssCredential.getClass());
}
GSSCredentialImpl gssCredImpl = (GSSCredentialImpl) gssCredential; return serialize(kerberosTicket);
Oid[] mechs = gssCredImpl.getMechs();
for (Oid oid : mechs) {
if (oid.equals(KRB5_OID)) {
int usage = gssCredImpl.getUsage(oid);
boolean initiate = (usage == GSSCredential.INITIATE_ONLY || usage == GSSCredential.INITIATE_AND_ACCEPT);
GSSCredentialSpi credentialSpi = gssCredImpl.getElement(oid, initiate);
if (credentialSpi instanceof Krb5InitCredential) {
Krb5InitCredential credential = (Krb5InitCredential) credentialSpi;
KerberosTicket kerberosTicket = new KerberosTicket(credential.getEncoded(),
credential.getClient(),
credential.getServer(),
credential.getSessionKey().getEncoded(),
credential.getSessionKeyType(),
credential.getFlags(),
credential.getAuthTime(),
credential.getStartTime(),
credential.getEndTime(),
credential.getRenewTill(),
credential.getClientAddresses());
return serialize(kerberosTicket);
} else {
throw new KerberosSerializationException("Unsupported type of credentialSpi: " + credentialSpi.getClass());
}
}
}
throw new KerberosSerializationException("Kerberos credential not found. Available mechanisms: " + mechs);
} catch (IOException e) { } catch (IOException e) {
throw new KerberosSerializationException("Exception occured", e); throw new KerberosSerializationException("Unexpected exception when serialize GSSCredential", e);
} catch (GSSException e) {
throw new KerberosSerializationException("Exception occured", e);
} }
} }
@ -132,32 +81,12 @@ public class KerberosSerializationUtils {
} }
KerberosTicket ticket = (KerberosTicket) deserializedCred; KerberosTicket ticket = (KerberosTicket) deserializedCred;
String fullName = ticket.getClient().getName();
Method getInstance = Reflections.findDeclaredMethod(Krb5NameElement.class, "getInstance", String.class, Oid.class); return KerberosJdkProvider.getProvider().kerberosTicketToGSSCredential(ticket);
Krb5NameElement krb5Name = Reflections.invokeMethod(true, getInstance, Krb5NameElement.class, null, fullName, KRB5_NAME_OID); } catch (KerberosSerializationException ke) {
throw ke;
Credentials krb5CredsInternal = new Credentials(
ticket.getEncoded(),
ticket.getClient().getName(),
ticket.getServer().getName(),
ticket.getSessionKey().getEncoded(),
ticket.getSessionKeyType(),
ticket.getFlags(),
ticket.getAuthTime(),
ticket.getStartTime(),
ticket.getEndTime(),
ticket.getRenewTill(),
ticket.getClientAddresses()
);
Method getInstance2 = Reflections.findDeclaredMethod(Krb5InitCredential.class, "getInstance", Krb5NameElement.class, Credentials.class);
Krb5InitCredential initCredential = Reflections.invokeMethod(true, getInstance2, Krb5InitCredential.class, null, krb5Name, krb5CredsInternal);
GSSManagerImpl manager = (GSSManagerImpl) GSSManager.getInstance();
return new GSSCredentialImpl(manager, initCredential);
} catch (Exception ioe) { } catch (Exception ioe) {
throw new KerberosSerializationException("Exception occured", ioe); throw new KerberosSerializationException("Unexpected exception when deserialize GSSCredential", ioe);
} }
} }

View file

@ -52,7 +52,7 @@ public abstract class CommonKerberosConfig {
return getConfig().get(KerberosConstants.KEYTAB); return getConfig().get(KerberosConstants.KEYTAB);
} }
public boolean getDebug() { public boolean isDebug() {
return Boolean.valueOf(getConfig().get(KerberosConstants.DEBUG)); return Boolean.valueOf(getConfig().get(KerberosConstants.DEBUG));
} }

View file

@ -17,16 +17,18 @@
package org.keycloak.federation.kerberos.impl; package org.keycloak.federation.kerberos.impl;
import java.util.HashMap; import java.io.IOException;
import java.util.Map;
import javax.security.auth.Subject; import javax.security.auth.Subject;
import javax.security.auth.login.AppConfigurationEntry; import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.Configuration; import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException; import javax.security.auth.login.LoginException;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.util.KerberosJdkProvider;
import org.keycloak.federation.kerberos.CommonKerberosConfig; import org.keycloak.federation.kerberos.CommonKerberosConfig;
/** /**
@ -36,20 +38,32 @@ public class KerberosServerSubjectAuthenticator {
private static final Logger logger = Logger.getLogger(KerberosServerSubjectAuthenticator.class); private static final Logger logger = Logger.getLogger(KerberosServerSubjectAuthenticator.class);
private static final CallbackHandler NO_CALLBACK_HANDLER = new CallbackHandler() {
@Override
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
throw new UnsupportedCallbackException(callbacks[0]);
}
};
private final CommonKerberosConfig config; private final CommonKerberosConfig config;
private LoginContext loginContext; private LoginContext loginContext;
public KerberosServerSubjectAuthenticator(CommonKerberosConfig config) { public KerberosServerSubjectAuthenticator(CommonKerberosConfig config) {
this.config = config; this.config = config;
} }
public Subject authenticateServerSubject() throws LoginException { public Subject authenticateServerSubject() throws LoginException {
Configuration config = createJaasConfiguration(); Configuration config = createJaasConfiguration();
loginContext = new LoginContext("does-not-matter", null, null, config); loginContext = new LoginContext("does-not-matter", null, NO_CALLBACK_HANDLER, config);
loginContext.login(); loginContext.login();
return loginContext.getSubject(); return loginContext.getSubject();
} }
public void logoutServerSubject() { public void logoutServerSubject() {
if (loginContext != null) { if (loginContext != null) {
try { try {
@ -60,24 +74,9 @@ public class KerberosServerSubjectAuthenticator {
} }
} }
protected Configuration createJaasConfiguration() { protected Configuration createJaasConfiguration() {
return new Configuration() { return KerberosJdkProvider.getProvider().createJaasConfigurationForServer(config.getKeyTab(), config.getServerPrincipal(), config.isDebug());
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
Map<String, Object> options = new HashMap<String, Object>();
options.put("storeKey", "true");
options.put("doNotPrompt", "true");
options.put("isInitiator", "false");
options.put("useKeyTab", "true");
options.put("keyTab", config.getKeyTab());
options.put("principal", config.getServerPrincipal());
options.put("debug", String.valueOf(config.getDebug()));
AppConfigurationEntry kerberosLMConfiguration = new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options);
return new AppConfigurationEntry[] { kerberosLMConfiguration };
}
};
} }
} }

View file

@ -33,6 +33,7 @@ import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException; import javax.security.auth.login.LoginException;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.util.KerberosJdkProvider;
import org.keycloak.federation.kerberos.CommonKerberosConfig; import org.keycloak.federation.kerberos.CommonKerberosConfig;
import org.keycloak.models.ModelException; import org.keycloak.models.ModelException;
@ -58,7 +59,7 @@ public class KerberosUsernamePasswordAuthenticator {
* @return true if user available * @return true if user available
*/ */
public boolean isUserAvailable(String username) { public boolean isUserAvailable(String username) {
logger.debug("Checking existence of user: " + username); logger.debugf("Checking existence of user: %s", username);
try { try {
String principal = getKerberosPrincipal(username); String principal = getKerberosPrincipal(username);
loginContext = new LoginContext("does-not-matter", null, loginContext = new LoginContext("does-not-matter", null,
@ -70,7 +71,7 @@ public class KerberosUsernamePasswordAuthenticator {
throw new IllegalStateException("Didn't expect to end here"); throw new IllegalStateException("Didn't expect to end here");
} catch (LoginException le) { } catch (LoginException le) {
String message = le.getMessage(); String message = le.getMessage();
logger.debug("Message from kerberos: " + message); logger.debugf("Message from kerberos: %s", message);
checkKerberosServerAvailable(le); checkKerberosServerAvailable(le);
@ -128,6 +129,7 @@ public class KerberosUsernamePasswordAuthenticator {
return loginContext.getSubject(); return loginContext.getSubject();
} }
public void logoutSubject() { public void logoutSubject() {
if (loginContext != null) { if (loginContext != null) {
try { try {
@ -139,7 +141,6 @@ public class KerberosUsernamePasswordAuthenticator {
} }
protected String getKerberosPrincipal(String username) throws LoginException { protected String getKerberosPrincipal(String username) throws LoginException {
if (username.contains("@")) { if (username.contains("@")) {
String[] tokens = username.split("@"); String[] tokens = username.split("@");
@ -156,6 +157,7 @@ public class KerberosUsernamePasswordAuthenticator {
return username + "@" + config.getKerberosRealm(); return username + "@" + config.getKerberosRealm();
} }
protected CallbackHandler createJaasCallbackHandler(final String principal, final String password) { protected CallbackHandler createJaasCallbackHandler(final String principal, final String password) {
return new CallbackHandler() { return new CallbackHandler() {
@ -176,17 +178,8 @@ public class KerberosUsernamePasswordAuthenticator {
}; };
} }
protected Configuration createJaasConfiguration() {
return new Configuration() {
@Override protected Configuration createJaasConfiguration() {
public AppConfigurationEntry[] getAppConfigurationEntry(String name) { return KerberosJdkProvider.getProvider().createJaasConfigurationForUsernamePasswordLogin(config.isDebug());
Map<String, Object> options = new HashMap<String, Object>();
options.put("storeKey", "true");
options.put("debug", String.valueOf(config.getDebug()));
AppConfigurationEntry kerberosLMConfiguration = new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options);
return new AppConfigurationEntry[] { kerberosLMConfiguration };
}
};
} }
} }

View file

@ -19,15 +19,21 @@ package org.keycloak.federation.kerberos.impl;
import java.io.IOException; import java.io.IOException;
import java.security.PrivilegedExceptionAction; import java.security.PrivilegedExceptionAction;
import java.util.Iterator;
import java.util.Set;
import javax.security.auth.Subject; import javax.security.auth.Subject;
import javax.security.auth.kerberos.KerberosTicket;
import org.ietf.jgss.Oid;
import org.keycloak.common.constants.KerberosConstants;
import org.keycloak.common.util.Base64; import org.keycloak.common.util.Base64;
import org.ietf.jgss.GSSContext; import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential; import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSException; import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager; import org.ietf.jgss.GSSManager;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.util.KerberosJdkProvider;
import org.keycloak.federation.kerberos.CommonKerberosConfig; import org.keycloak.federation.kerberos.CommonKerberosConfig;
import org.keycloak.common.util.KerberosSerializationUtils; import org.keycloak.common.util.KerberosSerializationUtils;
@ -45,6 +51,7 @@ public class SPNEGOAuthenticator {
private boolean authenticated = false; private boolean authenticated = false;
private String authenticatedKerberosPrincipal = null; private String authenticatedKerberosPrincipal = null;
private GSSCredential delegationCredential; private GSSCredential delegationCredential;
private KerberosTicket kerberosTicket;
private String responseToken = null; private String responseToken = null;
public SPNEGOAuthenticator(CommonKerberosConfig kerberosConfig, KerberosServerSubjectAuthenticator kerberosSubjectAuthenticator, String spnegoToken) { public SPNEGOAuthenticator(CommonKerberosConfig kerberosConfig, KerberosServerSubjectAuthenticator kerberosSubjectAuthenticator, String spnegoToken) {
@ -61,6 +68,14 @@ public class SPNEGOAuthenticator {
try { try {
Subject serverSubject = kerberosSubjectAuthenticator.authenticateServerSubject(); Subject serverSubject = kerberosSubjectAuthenticator.authenticateServerSubject();
authenticated = Subject.doAs(serverSubject, new AcceptSecContext()); authenticated = Subject.doAs(serverSubject, new AcceptSecContext());
// kerberosTicketis available in IBM JDK in case that GSSContext supports delegated credentials
Set<KerberosTicket> kerberosTickets = serverSubject.getPrivateCredentials(KerberosTicket.class);
Iterator<KerberosTicket> iterator = kerberosTickets.iterator();
if (iterator.hasNext()) {
kerberosTicket = iterator.next();
}
} catch (Exception e) { } catch (Exception e) {
log.warn("SPNEGO login failed", e); log.warn("SPNEGO login failed", e);
} finally { } finally {
@ -89,7 +104,7 @@ public class SPNEGOAuthenticator {
if (log.isTraceEnabled()) { if (log.isTraceEnabled()) {
log.trace("Serializing credential " + delegationCredential); log.trace("Serializing credential " + delegationCredential);
} }
return KerberosSerializationUtils.serializeCredential(delegationCredential); return KerberosSerializationUtils.serializeCredential(kerberosTicket, delegationCredential);
} catch (KerberosSerializationUtils.KerberosSerializationException kse) { } catch (KerberosSerializationUtils.KerberosSerializationException kse) {
log.warn("Couldn't serialize credential: " + delegationCredential, kse); log.warn("Couldn't serialize credential: " + delegationCredential, kse);
return null; return null;
@ -150,7 +165,10 @@ public class SPNEGOAuthenticator {
protected GSSContext establishContext() throws GSSException, IOException { protected GSSContext establishContext() throws GSSException, IOException {
GSSManager manager = GSSManager.getInstance(); GSSManager manager = GSSManager.getInstance();
GSSContext gssContext = manager.createContext((GSSCredential) null);
Oid[] supportedMechs = new Oid[] { KerberosConstants.KRB5_OID, KerberosConstants.SPNEGO_OID };
GSSCredential gssCredential = manager.createCredential(null, GSSCredential.INDEFINITE_LIFETIME, supportedMechs, GSSCredential.ACCEPT_ONLY);
GSSContext gssContext = manager.createContext(gssCredential);
byte[] inputToken = Base64.decode(spnegoToken); byte[] inputToken = Base64.decode(spnegoToken);
byte[] respToken = gssContext.acceptSecContext(inputToken, 0, inputToken.length); byte[] respToken = gssContext.acceptSecContext(inputToken, 0, inputToken.length);

View file

@ -192,6 +192,10 @@ public abstract class AbstractKerberosTest {
loginPage.login("jduke", "theduke"); loginPage.login("jduke", "theduke");
changePasswordPage.assertCurrent(); changePasswordPage.assertCurrent();
// Bad existing password
changePasswordPage.changePassword("theduke-invalid", "newPass", "newPass");
Assert.assertTrue(driver.getPageSource().contains("Invalid existing password."));
// Change password is not possible as editMode is READ_ONLY // Change password is not possible as editMode is READ_ONLY
changePasswordPage.changePassword("theduke", "newPass", "newPass"); changePasswordPage.changePassword("theduke", "newPass", "newPass");
Assert.assertTrue(driver.getPageSource().contains("You can't update your password as your account is read only")); Assert.assertTrue(driver.getPageSource().contains("You can't update your password as your account is read only"));

View file

@ -31,4 +31,4 @@ idm.test.kerberos.allow.kerberos.authentication=true
idm.test.kerberos.realm=KEYCLOAK.ORG idm.test.kerberos.realm=KEYCLOAK.ORG
idm.test.kerberos.server.principal=HTTP/localhost@KEYCLOAK.ORG idm.test.kerberos.server.principal=HTTP/localhost@KEYCLOAK.ORG
idm.test.kerberos.debug=false idm.test.kerberos.debug=false
idm.test.kerberos.use.kerberos.for.password.authentication=false idm.test.kerberos.use.kerberos.for.password.authentication=true

View file

@ -1,8 +1,8 @@
[libdefaults] [libdefaults]
default_realm = KEYCLOAK.ORG default_realm = KEYCLOAK.ORG
default_tgs_enctypes = des3-cbc-sha1-kd aes256-cts-hmac-sha1-96 rc4-hmac default_tgs_enctypes = des3-cbc-sha1-kd aes256-cts-hmac-sha1-96 rc4-hmac aes128-cts-hmac-sha1-96
default_tkt_enctypes = des3-cbc-sha1-kd aes256-cts-hmac-sha1-96 rc4-hmac default_tkt_enctypes = des3-cbc-sha1-kd aes256-cts-hmac-sha1-96 rc4-hmac aes128-cts-hmac-sha1-96
permitted_enctypes = des3-cbc-sha1-kd aes256-cts-hmac-sha1-96 rc4-hmac permitted_enctypes = des3-cbc-sha1-kd aes256-cts-hmac-sha1-96 rc4-hmac aes128-cts-hmac-sha1-96
kdc_timeout = 30000 kdc_timeout = 30000
dns_lookup_realm = false dns_lookup_realm = false
dns_lookup_kdc = false dns_lookup_kdc = false

View file

@ -19,8 +19,11 @@ package org.keycloak.util.ldap;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.Locale;
import java.util.Properties; import java.util.Properties;
import java.util.Set; import java.util.Set;
@ -43,13 +46,7 @@ import org.apache.directory.server.protocol.shared.transport.UdpTransport;
import org.apache.directory.shared.kerberos.KerberosTime; import org.apache.directory.shared.kerberos.KerberosTime;
import org.apache.directory.shared.kerberos.KerberosUtils; import org.apache.directory.shared.kerberos.KerberosUtils;
import org.apache.directory.shared.kerberos.codec.types.EncryptionType; import org.apache.directory.shared.kerberos.codec.types.EncryptionType;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.util.KerberosSerializationUtils;
import sun.security.jgss.GSSNameImpl;
import sun.security.jgss.krb5.Krb5NameElement;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -113,15 +110,8 @@ public class KerberosEmbeddedServer extends LDAPEmbeddedServer {
this.kdcEncryptionTypes = readProperty(PROPERTY_KDC_ENCTYPES, DEFAULT_KDC_ENCRYPTION_TYPES); this.kdcEncryptionTypes = readProperty(PROPERTY_KDC_ENCTYPES, DEFAULT_KDC_ENCRYPTION_TYPES);
if (ldapSaslPrincipal == null || ldapSaslPrincipal.isEmpty()) { if (ldapSaslPrincipal == null || ldapSaslPrincipal.isEmpty()) {
try { String hostname = getHostnameForSASLPrincipal(bindHost);
// Same algorithm like sun.security.krb5.PrincipalName constructor this.ldapSaslPrincipal = "ldap/" + hostname + "@" + this.kerberosRealm;
GSSName gssName = GSSManager.getInstance().createName("ldap@" + bindHost, GSSName.NT_HOSTBASED_SERVICE);
GSSNameImpl gssName1 = (GSSNameImpl) gssName;
Krb5NameElement krb5NameElement = (Krb5NameElement) gssName1.getElement(KerberosSerializationUtils.KRB5_OID);
this.ldapSaslPrincipal = krb5NameElement.getKrb5PrincipalName().toString();
} catch (GSSException uhe) {
throw new RuntimeException(uhe);
}
} }
} }
@ -219,6 +209,31 @@ public class KerberosEmbeddedServer extends LDAPEmbeddedServer {
} }
// Forked from sun.security.krb5.PrincipalName constructor
private String getHostnameForSASLPrincipal(String hostName) {
try {
// RFC4120 does not recommend canonicalizing a hostname.
// However, for compatibility reason, we will try
// canonicalize it and see if the output looks better.
String canonicalized = (InetAddress.getByName(hostName)).
getCanonicalHostName();
// Looks if canonicalized is a longer format of hostName,
// we accept cases like
// bunny -> bunny.rabbit.hole
if (canonicalized.toLowerCase(Locale.ENGLISH).startsWith(
hostName.toLowerCase(Locale.ENGLISH)+".")) {
hostName = canonicalized;
}
} catch (UnknownHostException | SecurityException e) {
// not canonicalized or no permission to do so, use old
}
return hostName.toLowerCase(Locale.ENGLISH);
}
/** /**
* Replacement of apacheDS KdcServer class with disabled ticket replay cache. * Replacement of apacheDS KdcServer class with disabled ticket replay cache.
* *