/*
* 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.util.ldap;
import org.apache.directory.api.ldap.model.constants.SupportedSaslMechanisms;
import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
import org.apache.directory.server.core.api.DirectoryService;
import org.apache.directory.server.core.kerberos.KeyDerivationInterceptor;
import org.apache.directory.server.kerberos.KerberosConfig;
import org.apache.directory.server.kerberos.kdc.KdcServer;
import org.apache.directory.server.kerberos.shared.replay.ReplayCache;
import org.apache.directory.server.ldap.LdapServer;
import org.apache.directory.server.ldap.handlers.sasl.cramMD5.CramMd5MechanismHandler;
import org.apache.directory.server.ldap.handlers.sasl.digestMD5.DigestMd5MechanismHandler;
import org.apache.directory.server.ldap.handlers.sasl.gssapi.GssapiMechanismHandler;
import org.apache.directory.server.ldap.handlers.sasl.ntlm.NtlmMechanismHandler;
import org.apache.directory.server.ldap.handlers.sasl.plain.PlainMechanismHandler;
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.jboss.logging.Logger;
import javax.security.auth.kerberos.KerberosPrincipal;
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;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class KerberosEmbeddedServer extends LDAPEmbeddedServer {
private static final Logger log = Logger.getLogger(KerberosEmbeddedServer.class);
public static final String PROPERTY_KERBEROS_REALM = "kerberos.realm";
public static final String PROPERTY_KDC_PORT = "kerberos.port";
public static final String PROPERTY_KDC_ENCTYPES = "kerberos.encTypes";
private static final String DEFAULT_KERBEROS_LDIF_FILE = "classpath:kerberos/default-users.ldif";
private static final String DEFAULT_KERBEROS_REALM = "KEYCLOAK.ORG";
private static final String DEFAULT_KDC_PORT = "6088";
private static final String DEFAULT_KDC_ENCRYPTION_TYPES = "aes128-cts-hmac-sha1-96, des-cbc-md5, des3-cbc-sha1-kd";
private final String kerberosRealm;
private final int kdcPort;
private final String kdcEncryptionTypes;
private KdcServer kdcServer;
public static void main(String[] args) throws Exception {
Properties defaultProperties = new Properties();
defaultProperties.put(PROPERTY_DSF, DSF_FILE);
execute(args, defaultProperties);
}
public static void execute(String[] args, Properties defaultProperties) throws Exception {
final KerberosEmbeddedServer kerberosEmbeddedServer = new KerberosEmbeddedServer(defaultProperties);
kerberosEmbeddedServer.init();
kerberosEmbeddedServer.start();
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
try {
kerberosEmbeddedServer.stop();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
public KerberosEmbeddedServer(Properties defaultProperties) {
super(defaultProperties);
this.ldifFile = readProperty(PROPERTY_LDIF_FILE, DEFAULT_KERBEROS_LDIF_FILE);
this.kerberosRealm = readProperty(PROPERTY_KERBEROS_REALM, DEFAULT_KERBEROS_REALM);
String kdcPort = readProperty(PROPERTY_KDC_PORT, DEFAULT_KDC_PORT);
this.kdcPort = Integer.parseInt(kdcPort);
this.kdcEncryptionTypes = readProperty(PROPERTY_KDC_ENCTYPES, DEFAULT_KDC_ENCRYPTION_TYPES);
if (ldapSaslPrincipal == null || ldapSaslPrincipal.isEmpty()) {
String hostname = getHostnameForSASLPrincipal(bindHost);
this.ldapSaslPrincipal = "ldap/" + hostname + "@" + this.kerberosRealm;
}
}
@Override
public void init() throws Exception {
super.init();
log.info("Creating KDC server. kerberosRealm: " + kerberosRealm + ", kdcPort: " + kdcPort + ", kdcEncryptionTypes: " + kdcEncryptionTypes);
createAndStartKdcServer();
}
@Override
protected DirectoryService createDirectoryService() throws Exception {
DirectoryService directoryService = super.createDirectoryService();
directoryService.addLast(new KeyDerivationInterceptor());
return directoryService;
}
@Override
protected LdapServer createLdapServer() {
LdapServer ldapServer = super.createLdapServer();
ldapServer.setSaslHost(this.bindHost);
ldapServer.setSaslPrincipal( this.ldapSaslPrincipal);
ldapServer.setSaslRealms(new ArrayList<String>());
ldapServer.addSaslMechanismHandler(SupportedSaslMechanisms.PLAIN, new PlainMechanismHandler());
ldapServer.addSaslMechanismHandler(SupportedSaslMechanisms.CRAM_MD5, new CramMd5MechanismHandler());
ldapServer.addSaslMechanismHandler(SupportedSaslMechanisms.DIGEST_MD5, new DigestMd5MechanismHandler());
ldapServer.addSaslMechanismHandler(SupportedSaslMechanisms.GSSAPI, new GssapiMechanismHandler());
ldapServer.addSaslMechanismHandler(SupportedSaslMechanisms.NTLM, new NtlmMechanismHandler());
ldapServer.addSaslMechanismHandler(SupportedSaslMechanisms.GSS_SPNEGO, new NtlmMechanismHandler());
return ldapServer;
}
protected KdcServer createAndStartKdcServer() throws Exception {
KerberosConfig kdcConfig = new KerberosConfig();
kdcConfig.setServicePrincipal("krbtgt/" + this.kerberosRealm + "@" + this.kerberosRealm);
kdcConfig.setPrimaryRealm(this.kerberosRealm);
kdcConfig.setMaximumTicketLifetime(60000 * 1440);
kdcConfig.setMaximumRenewableLifetime(60000 * 10080);
kdcConfig.setPaEncTimestampRequired(false);
Set<EncryptionType> encryptionTypes = convertEncryptionTypes();
kdcConfig.setEncryptionTypes(encryptionTypes);
kdcServer = new NoReplayKdcServer(kdcConfig);
kdcServer.setSearchBaseDn(this.baseDN);
UdpTransport udp = new UdpTransport(this.bindHost, this.kdcPort);
kdcServer.addTransports(udp);
kdcServer.setDirectoryService(directoryService);
// Launch the server
kdcServer.start();
return kdcServer;
}
public void stop() throws Exception {
stopLdapServer();
stopKerberosServer();
shutdownDirectoryService();
}
protected void stopKerberosServer() {
log.info("Stopping Kerberos server.");
kdcServer.stop();
}
private Set<EncryptionType> convertEncryptionTypes() {
Set<EncryptionType> encryptionTypes = new HashSet<EncryptionType>();
String[] configEncTypes = kdcEncryptionTypes.split(",");
for ( String enc : configEncTypes ) {
enc = enc.trim();
for ( EncryptionType type : EncryptionType.getEncryptionTypes() ) {
if ( type.getName().equalsIgnoreCase( enc ) ) {
encryptionTypes.add( type );
}
}
}
encryptionTypes = KerberosUtils.orderEtypesByStrength(encryptionTypes);
return encryptionTypes;
}
// 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.
*
* @author Dominik Pospisil <dpospisi@redhat.com>
*/
class NoReplayKdcServer extends KdcServer {
NoReplayKdcServer(KerberosConfig kdcConfig) {
super(kdcConfig);
}
/**
*
* Dummy implementation of the ApacheDS kerberos replay cache. Essentially disables kerbores ticket replay checks.
* https://issues.jboss.org/browse/JBPAPP-10974
*
* @author Dominik Pospisil <dpospisi@redhat.com>
*/
private class DummyReplayCache implements ReplayCache {
@Override
public boolean isReplay(KerberosPrincipal serverPrincipal, KerberosPrincipal clientPrincipal, KerberosTime clientTime,
int clientMicroSeconds) {
return false;
}
@Override
public void save(KerberosPrincipal serverPrincipal, KerberosPrincipal clientPrincipal, KerberosTime clientTime,
int clientMicroSeconds) {
}
@Override
public void clear() {
}
}
/**
* @throws java.io.IOException if we cannot bind to the sockets
*/
@Override
public void start() throws IOException, LdapInvalidDnException {
super.start();
try {
// override initialized replay cache with a dummy implementation
Field replayCacheField = KdcServer.class.getDeclaredField("replayCache");
replayCacheField.setAccessible(true);
replayCacheField.set(this, new DummyReplayCache());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}