From 7f32ce810a78fbc06272d7f7efa525bb06967886 Mon Sep 17 00:00:00 2001 From: mposolda Date: Wed, 24 Feb 2016 22:15:04 +0100 Subject: [PATCH] KEYCLOAK-1928 Kerberos working with IBM JDK --- .../common/constants/KerberosConstants.java | 23 +- .../common/util/KerberosJdkProvider.java | 227 ++++++++++++++++++ .../util/KerberosSerializationUtils.java | 93 +------ .../kerberos/CommonKerberosConfig.java | 2 +- .../KerberosServerSubjectAuthenticator.java | 41 ++-- ...KerberosUsernamePasswordAuthenticator.java | 21 +- .../kerberos/impl/SPNEGOAuthenticator.java | 22 +- .../federation/AbstractKerberosTest.java | 4 + .../kerberos-ldap-connection.properties | 2 +- .../test/resources/kerberos/test-krb5.conf | 6 +- .../util/ldap/KerberosEmbeddedServer.java | 45 ++-- 11 files changed, 344 insertions(+), 142 deletions(-) create mode 100644 common/src/main/java/org/keycloak/common/util/KerberosJdkProvider.java diff --git a/common/src/main/java/org/keycloak/common/constants/KerberosConstants.java b/common/src/main/java/org/keycloak/common/constants/KerberosConstants.java index 4089a0e018..b644f167ae 100644 --- a/common/src/main/java/org/keycloak/common/constants/KerberosConstants.java +++ b/common/src/main/java/org/keycloak/common/constants/KerberosConstants.java @@ -17,6 +17,9 @@ package org.keycloak.common.constants; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.Oid; + /** * @author Marek Posolda */ @@ -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 */ - 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 */ - 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 */ - 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); + } + } /** diff --git a/common/src/main/java/org/keycloak/common/util/KerberosJdkProvider.java b/common/src/main/java/org/keycloak/common/util/KerberosJdkProvider.java new file mode 100644 index 0000000000..82e214afc2 --- /dev/null +++ b/common/src/main/java/org/keycloak/common/util/KerberosJdkProvider.java @@ -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 Marek Posolda + */ +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 principals = Collections.singleton(kerberosPrincipal); + Set publicCreds = Collections.singleton(gssName); + Set privateCreds = Collections.singleton(kerberosTicket); + Subject subject = new Subject(false, principals, publicCreds, privateCreds); + + return Subject.doAs(subject, new PrivilegedExceptionAction() { + + @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 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 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 kerberosTickets = subject.getPrivateCredentials(KerberosTicket.class); + Iterator 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 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 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; + } + } + } +} diff --git a/common/src/main/java/org/keycloak/common/util/KerberosSerializationUtils.java b/common/src/main/java/org/keycloak/common/util/KerberosSerializationUtils.java index bc9d174d84..17a11e492e 100644 --- a/common/src/main/java/org/keycloak/common/util/KerberosSerializationUtils.java +++ b/common/src/main/java/org/keycloak/common/util/KerberosSerializationUtils.java @@ -25,22 +25,13 @@ import java.io.ObjectInputStream; import java.io.ObjectOutput; import java.io.ObjectOutputStream; 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 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 @@ -50,18 +41,9 @@ import sun.security.krb5.Credentials; */ public class KerberosSerializationUtils { - public static final Oid KRB5_OID; - public static final Oid KRB5_NAME_OID; public static final String JAVA_INFO; 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 javaRuntimeVersion = System.getProperty("java.runtime.version"); String javaVendor = System.getProperty("java.vendor"); @@ -72,50 +54,17 @@ public class KerberosSerializationUtils { private KerberosSerializationUtils() { } - public static String serializeCredential(GSSCredential gssCredential) throws KerberosSerializationException { + public static String serializeCredential(KerberosTicket kerberosTicket, GSSCredential gssCredential) throws KerberosSerializationException { try { if (gssCredential == null) { throw new KerberosSerializationException("Null credential given as input"); } - if (!(gssCredential instanceof GSSCredentialImpl)) { - throw new KerberosSerializationException("Unknown credential type: " + gssCredential.getClass()); - } + kerberosTicket = KerberosJdkProvider.getProvider().gssCredentialToKerberosTicket(kerberosTicket, gssCredential); - GSSCredentialImpl gssCredImpl = (GSSCredentialImpl) gssCredential; - 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); + return serialize(kerberosTicket); } catch (IOException e) { - throw new KerberosSerializationException("Exception occured", e); - } catch (GSSException e) { - throw new KerberosSerializationException("Exception occured", e); + throw new KerberosSerializationException("Unexpected exception when serialize GSSCredential", e); } } @@ -132,32 +81,12 @@ public class KerberosSerializationUtils { } KerberosTicket ticket = (KerberosTicket) deserializedCred; - String fullName = ticket.getClient().getName(); - Method getInstance = Reflections.findDeclaredMethod(Krb5NameElement.class, "getInstance", String.class, Oid.class); - Krb5NameElement krb5Name = Reflections.invokeMethod(true, getInstance, Krb5NameElement.class, null, fullName, KRB5_NAME_OID); - - 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); + return KerberosJdkProvider.getProvider().kerberosTicketToGSSCredential(ticket); + } catch (KerberosSerializationException ke) { + throw ke; } catch (Exception ioe) { - throw new KerberosSerializationException("Exception occured", ioe); + throw new KerberosSerializationException("Unexpected exception when deserialize GSSCredential", ioe); } } diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/CommonKerberosConfig.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/CommonKerberosConfig.java index c2eccde759..4d3bf62e12 100644 --- a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/CommonKerberosConfig.java +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/CommonKerberosConfig.java @@ -52,7 +52,7 @@ public abstract class CommonKerberosConfig { return getConfig().get(KerberosConstants.KEYTAB); } - public boolean getDebug() { + public boolean isDebug() { return Boolean.valueOf(getConfig().get(KerberosConstants.DEBUG)); } diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosServerSubjectAuthenticator.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosServerSubjectAuthenticator.java index 2a155ed485..aeaa07414c 100644 --- a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosServerSubjectAuthenticator.java +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosServerSubjectAuthenticator.java @@ -17,16 +17,18 @@ package org.keycloak.federation.kerberos.impl; -import java.util.HashMap; -import java.util.Map; +import java.io.IOException; 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.LoginContext; import javax.security.auth.login.LoginException; import org.jboss.logging.Logger; +import org.keycloak.common.util.KerberosJdkProvider; 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 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 LoginContext loginContext; + public KerberosServerSubjectAuthenticator(CommonKerberosConfig config) { this.config = config; } + public Subject authenticateServerSubject() throws LoginException { 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(); return loginContext.getSubject(); } + public void logoutServerSubject() { if (loginContext != null) { try { @@ -60,24 +74,9 @@ public class KerberosServerSubjectAuthenticator { } } + protected Configuration createJaasConfiguration() { - return new Configuration() { - - @Override - public AppConfigurationEntry[] getAppConfigurationEntry(String name) { - Map options = new HashMap(); - 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 }; - } - }; + return KerberosJdkProvider.getProvider().createJaasConfigurationForServer(config.getKeyTab(), config.getServerPrincipal(), config.isDebug()); } } diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosUsernamePasswordAuthenticator.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosUsernamePasswordAuthenticator.java index f18cf2ceb1..2254a6e87f 100644 --- a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosUsernamePasswordAuthenticator.java +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosUsernamePasswordAuthenticator.java @@ -33,6 +33,7 @@ import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import org.jboss.logging.Logger; +import org.keycloak.common.util.KerberosJdkProvider; import org.keycloak.federation.kerberos.CommonKerberosConfig; import org.keycloak.models.ModelException; @@ -58,7 +59,7 @@ public class KerberosUsernamePasswordAuthenticator { * @return true if user available */ public boolean isUserAvailable(String username) { - logger.debug("Checking existence of user: " + username); + logger.debugf("Checking existence of user: %s", username); try { String principal = getKerberosPrincipal(username); loginContext = new LoginContext("does-not-matter", null, @@ -70,7 +71,7 @@ public class KerberosUsernamePasswordAuthenticator { throw new IllegalStateException("Didn't expect to end here"); } catch (LoginException le) { String message = le.getMessage(); - logger.debug("Message from kerberos: " + message); + logger.debugf("Message from kerberos: %s", message); checkKerberosServerAvailable(le); @@ -128,6 +129,7 @@ public class KerberosUsernamePasswordAuthenticator { return loginContext.getSubject(); } + public void logoutSubject() { if (loginContext != null) { try { @@ -139,7 +141,6 @@ public class KerberosUsernamePasswordAuthenticator { } - protected String getKerberosPrincipal(String username) throws LoginException { if (username.contains("@")) { String[] tokens = username.split("@"); @@ -156,6 +157,7 @@ public class KerberosUsernamePasswordAuthenticator { return username + "@" + config.getKerberosRealm(); } + protected CallbackHandler createJaasCallbackHandler(final String principal, final String password) { return new CallbackHandler() { @@ -176,17 +178,8 @@ public class KerberosUsernamePasswordAuthenticator { }; } - protected Configuration createJaasConfiguration() { - return new Configuration() { - @Override - public AppConfigurationEntry[] getAppConfigurationEntry(String name) { - Map options = new HashMap(); - 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 }; - } - }; + protected Configuration createJaasConfiguration() { + return KerberosJdkProvider.getProvider().createJaasConfigurationForUsernamePasswordLogin(config.isDebug()); } } diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/SPNEGOAuthenticator.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/SPNEGOAuthenticator.java index f09ea6eea6..c2b928ec0e 100644 --- a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/SPNEGOAuthenticator.java +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/SPNEGOAuthenticator.java @@ -19,15 +19,21 @@ package org.keycloak.federation.kerberos.impl; import java.io.IOException; import java.security.PrivilegedExceptionAction; +import java.util.Iterator; +import java.util.Set; 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.ietf.jgss.GSSContext; import org.ietf.jgss.GSSCredential; import org.ietf.jgss.GSSException; import org.ietf.jgss.GSSManager; import org.jboss.logging.Logger; +import org.keycloak.common.util.KerberosJdkProvider; import org.keycloak.federation.kerberos.CommonKerberosConfig; import org.keycloak.common.util.KerberosSerializationUtils; @@ -45,6 +51,7 @@ public class SPNEGOAuthenticator { private boolean authenticated = false; private String authenticatedKerberosPrincipal = null; private GSSCredential delegationCredential; + private KerberosTicket kerberosTicket; private String responseToken = null; public SPNEGOAuthenticator(CommonKerberosConfig kerberosConfig, KerberosServerSubjectAuthenticator kerberosSubjectAuthenticator, String spnegoToken) { @@ -61,6 +68,14 @@ public class SPNEGOAuthenticator { try { Subject serverSubject = kerberosSubjectAuthenticator.authenticateServerSubject(); authenticated = Subject.doAs(serverSubject, new AcceptSecContext()); + + // kerberosTicketis available in IBM JDK in case that GSSContext supports delegated credentials + Set kerberosTickets = serverSubject.getPrivateCredentials(KerberosTicket.class); + Iterator iterator = kerberosTickets.iterator(); + if (iterator.hasNext()) { + kerberosTicket = iterator.next(); + } + } catch (Exception e) { log.warn("SPNEGO login failed", e); } finally { @@ -89,7 +104,7 @@ public class SPNEGOAuthenticator { if (log.isTraceEnabled()) { log.trace("Serializing credential " + delegationCredential); } - return KerberosSerializationUtils.serializeCredential(delegationCredential); + return KerberosSerializationUtils.serializeCredential(kerberosTicket, delegationCredential); } catch (KerberosSerializationUtils.KerberosSerializationException kse) { log.warn("Couldn't serialize credential: " + delegationCredential, kse); return null; @@ -150,7 +165,10 @@ public class SPNEGOAuthenticator { protected GSSContext establishContext() throws GSSException, IOException { 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[] respToken = gssContext.acceptSecContext(inputToken, 0, inputToken.length); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java index 16e302ee0e..12f0f5ed11 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java @@ -192,6 +192,10 @@ public abstract class AbstractKerberosTest { loginPage.login("jduke", "theduke"); 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 changePasswordPage.changePassword("theduke", "newPass", "newPass"); Assert.assertTrue(driver.getPageSource().contains("You can't update your password as your account is read only")); diff --git a/testsuite/integration/src/test/resources/kerberos/kerberos-ldap-connection.properties b/testsuite/integration/src/test/resources/kerberos/kerberos-ldap-connection.properties index a0ea53789f..3170d6ce9c 100644 --- a/testsuite/integration/src/test/resources/kerberos/kerberos-ldap-connection.properties +++ b/testsuite/integration/src/test/resources/kerberos/kerberos-ldap-connection.properties @@ -31,4 +31,4 @@ idm.test.kerberos.allow.kerberos.authentication=true idm.test.kerberos.realm=KEYCLOAK.ORG idm.test.kerberos.server.principal=HTTP/localhost@KEYCLOAK.ORG idm.test.kerberos.debug=false -idm.test.kerberos.use.kerberos.for.password.authentication=false \ No newline at end of file +idm.test.kerberos.use.kerberos.for.password.authentication=true \ No newline at end of file diff --git a/testsuite/integration/src/test/resources/kerberos/test-krb5.conf b/testsuite/integration/src/test/resources/kerberos/test-krb5.conf index a775b47c86..6989b7f5e6 100644 --- a/testsuite/integration/src/test/resources/kerberos/test-krb5.conf +++ b/testsuite/integration/src/test/resources/kerberos/test-krb5.conf @@ -1,8 +1,8 @@ [libdefaults] default_realm = KEYCLOAK.ORG - default_tgs_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 - permitted_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 aes128-cts-hmac-sha1-96 + permitted_enctypes = des3-cbc-sha1-kd aes256-cts-hmac-sha1-96 rc4-hmac aes128-cts-hmac-sha1-96 kdc_timeout = 30000 dns_lookup_realm = false dns_lookup_kdc = false diff --git a/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/KerberosEmbeddedServer.java b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/KerberosEmbeddedServer.java index 1f8fe07154..d7f868f032 100644 --- a/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/KerberosEmbeddedServer.java +++ b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/KerberosEmbeddedServer.java @@ -19,8 +19,11 @@ package org.keycloak.util.ldap; import java.io.IOException; import java.lang.reflect.Field; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.HashSet; +import java.util.Locale; import java.util.Properties; 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.KerberosUtils; 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.keycloak.common.util.KerberosSerializationUtils; -import sun.security.jgss.GSSNameImpl; -import sun.security.jgss.krb5.Krb5NameElement; /** * @author Marek Posolda @@ -113,15 +110,8 @@ public class KerberosEmbeddedServer extends LDAPEmbeddedServer { this.kdcEncryptionTypes = readProperty(PROPERTY_KDC_ENCTYPES, DEFAULT_KDC_ENCRYPTION_TYPES); if (ldapSaslPrincipal == null || ldapSaslPrincipal.isEmpty()) { - try { - // Same algorithm like sun.security.krb5.PrincipalName constructor - 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); - } + String hostname = getHostnameForSASLPrincipal(bindHost); + this.ldapSaslPrincipal = "ldap/" + hostname + "@" + this.kerberosRealm; } } @@ -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. *