KEYCLOAK-1928 Kerberos working with IBM JDK
This commit is contained in:
parent
13acd512b6
commit
7f32ce810a
11 changed files with 344 additions and 142 deletions
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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"));
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in a new issue