package com.hwlcn.ldap.ldap.sdk; import java.io.File; import java.io.FileWriter; import java.io.PrintWriter; import java.security.PrivilegedExceptionAction; import java.util.HashMap; import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; import javax.security.auth.Subject; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.PasswordCallback; import javax.security.auth.login.LoginContext; import javax.security.sasl.RealmCallback; import javax.security.sasl.Sasl; import javax.security.sasl.SaslClient; import com.hwlcn.ldap.asn1.ASN1OctetString; import com.hwlcn.ldap.util.DebugType; import com.hwlcn.core.annotation.InternalUseOnly; import com.hwlcn.core.annotation.NotMutable; import com.hwlcn.core.annotation.ThreadSafety; import com.hwlcn.ldap.util.ThreadSafetyLevel; import static com.hwlcn.ldap.ldap.sdk.LDAPMessages.*; import static com.hwlcn.ldap.util.Debug.*; import static com.hwlcn.ldap.util.StaticUtils.*; import static com.hwlcn.ldap.util.Validator.*; /** * This class provides a SASL GSSAPI bind request implementation as described in * <A HREF="http://www.ietf.org/rfc/rfc4752.txt">RFC 4752</A>. It provides the * ability to authenticate to a directory server using Kerberos V, which can * serve as a kind of single sign-on mechanism that may be shared across * client applications that support Kerberos. At present, this implementation * may only be used for authentication, as it does not yet offer support for * integrity or confidentiality. * <BR><BR> * This class uses the Java Authentication and Authorization Service (JAAS) * behind the scenes to perform all Kerberos processing. This framework * requires a configuration file to indicate the underlying mechanism to be * used. It is possible for clients to explicitly specify the path to the * configuration file that should be used, but if none is given then a default * file will be created and used. This default file should be sufficient for * Sun-provided JVMs, but a custom file may be required for JVMs provided by * other vendors. * <BR><BR> * Elements included in a GSSAPI bind request include: * <UL> * <LI>Authentication ID -- A string which identifies the user that is * attempting to authenticate. It should be the user's Kerberos * principal.</LI> * <LI>Authorization ID -- An optional string which specifies an alternate * authorization identity that should be used for subsequent operations * requested on the connection. Like the authentication ID, the * authorization ID should be a Kerberos principal.</LI> * <LI>KDC Address -- An optional string which specifies the IP address or * resolvable name for the Kerberos key distribution center. If this is * not provided, an attempt will be made to determine the appropriate * value from the system configuration.</LI> * <LI>Realm -- An optional string which specifies the realm into which the * user should authenticate. If this is not provided, an attempt will be * made to determine the appropriate value from the system * configuration</LI> * <LI>Password -- The clear-text password for the target user in the Kerberos * realm.</LI> * </UL> * <H2>Example</H2> * The following example demonstrates the process for performing a GSSAPI bind * against a directory server with a username of "john.doe" and a password * of "password": * <PRE> * GSSAPIBindRequest bindRequest = * new GSSAPIBindRequest("john.doe@EXAMPLE.COM", "password"); * try * { * BindResult bindResult = connection.bind(bindRequest); * // If we get here, then the bind was successful. * } * catch (LDAPException le) * { * // The bind failed for some reason. * } * </PRE> */ @NotMutable() @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) public final class GSSAPIBindRequest extends SASLBindRequest implements CallbackHandler, PrivilegedExceptionAction<Object> { public static final String GSSAPI_MECHANISM_NAME = "GSSAPI"; private static final String PROPERTY_KDC_ADDRESS = "java.security.krb5.kdc"; private static final String PROPERTY_REALM = "java.security.krb5.realm"; private static final String PROPERTY_CONFIG_FILE = "java.security.auth.login.config"; private static final String PROPERTY_SUBJECT_CREDS_ONLY = "javax.security.auth.useSubjectCredsOnly"; private static final String DEFAULT_CONFIG_FILE = System.getProperty(PROPERTY_CONFIG_FILE); private static final String DEFAULT_KDC_ADDRESS = System.getProperty(PROPERTY_KDC_ADDRESS); private static final String DEFAULT_REALM = System.getProperty(PROPERTY_REALM); private static final String JAAS_CLIENT_NAME = "GSSAPIBindRequest"; private static final long serialVersionUID = 2511890818146955112L; private final ASN1OctetString password; private final AtomicReference<LDAPConnection> conn; private final boolean enableGSSAPIDebugging; private boolean renewTGT; private boolean requireCachedCredentials; private boolean useTicketCache; private int messageID; private final String authenticationID; private final String authorizationID; private final String configFilePath; private final String kdcAddress; private final String realm; private final String servicePrincipalProtocol; private String ticketCachePath; public GSSAPIBindRequest(final String authenticationID, final String password) throws LDAPException { this(new GSSAPIBindRequestProperties(authenticationID, password)); } public GSSAPIBindRequest(final String authenticationID, final byte[] password) throws LDAPException { this(new GSSAPIBindRequestProperties(authenticationID, password)); } public GSSAPIBindRequest(final String authenticationID, final String password, final Control[] controls) throws LDAPException { this(new GSSAPIBindRequestProperties(authenticationID, password), controls); } public GSSAPIBindRequest(final String authenticationID, final byte[] password, final Control[] controls) throws LDAPException { this(new GSSAPIBindRequestProperties(authenticationID, password), controls); } public GSSAPIBindRequest(final String authenticationID, final String authorizationID, final String password, final String realm, final String kdcAddress, final String configFilePath) throws LDAPException { this(new GSSAPIBindRequestProperties(authenticationID, authorizationID, new ASN1OctetString(password), realm, kdcAddress, configFilePath)); } public GSSAPIBindRequest(final String authenticationID, final String authorizationID, final byte[] password, final String realm, final String kdcAddress, final String configFilePath) throws LDAPException { this(new GSSAPIBindRequestProperties(authenticationID, authorizationID, new ASN1OctetString(password), realm, kdcAddress, configFilePath)); } public GSSAPIBindRequest(final String authenticationID, final String authorizationID, final String password, final String realm, final String kdcAddress, final String configFilePath, final Control[] controls) throws LDAPException { this(new GSSAPIBindRequestProperties(authenticationID, authorizationID, new ASN1OctetString(password), realm, kdcAddress, configFilePath), controls); } public GSSAPIBindRequest(final String authenticationID, final String authorizationID, final byte[] password, final String realm, final String kdcAddress, final String configFilePath, final Control[] controls) throws LDAPException { this(new GSSAPIBindRequestProperties(authenticationID, authorizationID, new ASN1OctetString(password), realm, kdcAddress, configFilePath), controls); } public GSSAPIBindRequest(final GSSAPIBindRequestProperties gssapiProperties, final Control... controls) throws LDAPException { super(controls); ensureNotNull(gssapiProperties); authenticationID = gssapiProperties.getAuthenticationID(); password = gssapiProperties.getPassword(); realm = gssapiProperties.getRealm(); kdcAddress = gssapiProperties.getKDCAddress(); servicePrincipalProtocol = gssapiProperties.getServicePrincipalProtocol(); enableGSSAPIDebugging = gssapiProperties.enableGSSAPIDebugging(); useTicketCache = gssapiProperties.useTicketCache(); requireCachedCredentials = gssapiProperties.requireCachedCredentials(); renewTGT = gssapiProperties.renewTGT(); ticketCachePath = gssapiProperties.getTicketCachePath(); conn = new AtomicReference<LDAPConnection>(); messageID = -1; final String authzID = gssapiProperties.getAuthorizationID(); if (authzID == null) { authorizationID = null; } else { authorizationID = authzID; } final String cfgPath = gssapiProperties.getConfigFilePath(); if (cfgPath == null) { if (DEFAULT_CONFIG_FILE == null) { configFilePath = getConfigFilePath(gssapiProperties); } else { configFilePath = DEFAULT_CONFIG_FILE; } } else { configFilePath = cfgPath; } } @Override() public String getSASLMechanismName() { return GSSAPI_MECHANISM_NAME; } public String getAuthenticationID() { return authenticationID; } public String getAuthorizationID() { return authorizationID; } public String getPasswordString() { if (password == null) { return null; } else { return password.stringValue(); } } public byte[] getPasswordBytes() { if (password == null) { return null; } else { return password.getValue(); } } public String getRealm() { return realm; } public String getKDCAddress() { return kdcAddress; } public String getConfigFilePath() { return configFilePath; } public String getServicePrincipalProtocol() { return servicePrincipalProtocol; } public boolean useTicketCache() { return useTicketCache; } public boolean requireCachedCredentials() { return requireCachedCredentials; } public String getTicketCachePath() { return ticketCachePath; } public boolean renewTGT() { return renewTGT; } public boolean enableGSSAPIDebugging() { return enableGSSAPIDebugging; } private static String getConfigFilePath( final GSSAPIBindRequestProperties properties) throws LDAPException { try { final File f = File.createTempFile("GSSAPIBindRequest-JAAS-Config-", ".conf"); f.deleteOnExit(); final PrintWriter w = new PrintWriter(new FileWriter(f)); try { try { final Class<?> sunModuleClass = Class.forName("com.sun.security.auth.module.Krb5LoginModule"); if (sunModuleClass != null) { writeSunJAASConfig(w, properties); return f.getAbsolutePath(); } } catch (final ClassNotFoundException cnfe) { debugException(cnfe); } try { final Class<?> ibmModuleClass = Class.forName("com.ibm.security.auth.module.Krb5LoginModule"); if (ibmModuleClass != null) { writeIBMJAASConfig(w, properties); return f.getAbsolutePath(); } } catch (final ClassNotFoundException cnfe) { debugException(cnfe); } throw new LDAPException(ResultCode.LOCAL_ERROR, ERR_GSSAPI_CANNOT_CREATE_JAAS_CONFIG.get( ERR_GSSAPI_NO_SUPPORTED_JAAS_MODULE.get())); } finally { w.close(); } } catch (final LDAPException le) { debugException(le); throw le; } catch (final Exception e) { debugException(e); throw new LDAPException(ResultCode.LOCAL_ERROR, ERR_GSSAPI_CANNOT_CREATE_JAAS_CONFIG.get(getExceptionMessage(e)), e); } } private static void writeSunJAASConfig(final PrintWriter w, final GSSAPIBindRequestProperties p) { w.println(JAAS_CLIENT_NAME + " {"); w.println(" com.sun.security.auth.module.Krb5LoginModule required"); w.println(" client=true"); if (p.useTicketCache()) { w.println(" useTicketCache=true"); w.println(" renewTGT=" + p.renewTGT()); w.println(" doNotPrompt=" + p.requireCachedCredentials()); final String ticketCachePath = p.getTicketCachePath(); if (ticketCachePath != null) { w.println(" ticketCache=\"" + ticketCachePath + '"'); } } else { w.println(" useTicketCache=false"); } if (p.enableGSSAPIDebugging()) { w.println(" debug=true"); } w.println(" ;"); w.println("};"); } private static void writeIBMJAASConfig(final PrintWriter w, final GSSAPIBindRequestProperties p) { w.println(JAAS_CLIENT_NAME + " {"); w.println(" com.ibm.security.auth.module.Krb5LoginModule required"); w.println(" credsType=initiator"); if (p.useTicketCache()) { final String ticketCachePath = p.getTicketCachePath(); if (ticketCachePath == null) { if (p.requireCachedCredentials()) { w.println(" useDefaultCcache=true"); } } else { final File f = new File(ticketCachePath); final String path = f.getAbsolutePath().replace('\\', '/'); w.println(" useCcache=\"file://" + path + '"'); } } else { w.println(" useDefaultCcache=false"); } if (p.enableGSSAPIDebugging()) { w.println(" debug=true"); } w.println(" ;"); w.println("};"); } @Override() protected BindResult process(final LDAPConnection connection, final int depth) throws LDAPException { if (! conn.compareAndSet(null, connection)) { throw new LDAPException(ResultCode.LOCAL_ERROR, ERR_GSSAPI_MULTIPLE_CONCURRENT_REQUESTS.get()); } System.setProperty(PROPERTY_CONFIG_FILE, configFilePath); System.setProperty(PROPERTY_SUBJECT_CREDS_ONLY, "true"); if (debugEnabled(DebugType.LDAP)) { debug(Level.CONFIG, DebugType.LDAP, "Using config file property " + PROPERTY_CONFIG_FILE + " = '" + configFilePath + "'."); debug(Level.CONFIG, DebugType.LDAP, "Using subject creds only property " + PROPERTY_SUBJECT_CREDS_ONLY + " = 'true'."); } if (kdcAddress == null) { if (DEFAULT_KDC_ADDRESS == null) { System.clearProperty(PROPERTY_KDC_ADDRESS); if (debugEnabled(DebugType.LDAP)) { debug(Level.CONFIG, DebugType.LDAP, "Clearing kdcAddress property '" + PROPERTY_KDC_ADDRESS + "'."); } } else { System.setProperty(PROPERTY_KDC_ADDRESS, DEFAULT_KDC_ADDRESS); if (debugEnabled(DebugType.LDAP)) { debug(Level.CONFIG, DebugType.LDAP, "Using default kdcAddress property " + PROPERTY_KDC_ADDRESS + " = '" + DEFAULT_KDC_ADDRESS + "'."); } } } else { System.setProperty(PROPERTY_KDC_ADDRESS, kdcAddress); if (debugEnabled(DebugType.LDAP)) { debug(Level.CONFIG, DebugType.LDAP, "Using kdcAddress property " + PROPERTY_KDC_ADDRESS + " = '" + kdcAddress + "'."); } } if (realm == null) { if (DEFAULT_REALM == null) { System.clearProperty(PROPERTY_REALM); if (debugEnabled(DebugType.LDAP)) { debug(Level.CONFIG, DebugType.LDAP, "Clearing realm property '" + PROPERTY_REALM + "'."); } } else { System.setProperty(PROPERTY_REALM, DEFAULT_REALM); if (debugEnabled(DebugType.LDAP)) { debug(Level.CONFIG, DebugType.LDAP, "Using default realm property " + PROPERTY_REALM + " = '" + DEFAULT_REALM + "'."); } } } else { System.setProperty(PROPERTY_REALM, realm); if (debugEnabled(DebugType.LDAP)) { debug(Level.CONFIG, DebugType.LDAP, "Using realm property " + PROPERTY_REALM + " = '" + realm + "'."); } } try { final LoginContext context; try { context = new LoginContext(JAAS_CLIENT_NAME, this); context.login(); } catch (Exception e) { debugException(e); throw new LDAPException(ResultCode.LOCAL_ERROR, ERR_GSSAPI_CANNOT_INITIALIZE_JAAS_CONTEXT.get( getExceptionMessage(e)), e); } try { return (BindResult) Subject.doAs(context.getSubject(), this); } catch (Exception e) { debugException(e); if (e instanceof LDAPException) { throw (LDAPException) e; } else { throw new LDAPException(ResultCode.LOCAL_ERROR, ERR_GSSAPI_AUTHENTICATION_FAILED.get( getExceptionMessage(e)), e); } } } finally { conn.set(null); } } @InternalUseOnly() public Object run() throws LDAPException { final LDAPConnection connection = conn.get(); final String[] mechanisms = { GSSAPI_MECHANISM_NAME }; final HashMap<String,Object> saslProperties = new HashMap<String,Object>(2); saslProperties.put(Sasl.QOP, "auth"); saslProperties.put(Sasl.SERVER_AUTH, "true"); final SaslClient saslClient; try { saslClient = Sasl.createSaslClient(mechanisms, authorizationID, servicePrincipalProtocol, connection.getConnectedAddress(), saslProperties, this); } catch (Exception e) { debugException(e); throw new LDAPException(ResultCode.LOCAL_ERROR, ERR_GSSAPI_CANNOT_CREATE_SASL_CLIENT.get(getExceptionMessage(e)), e); } final SASLHelper helper = new SASLHelper(this, connection, GSSAPI_MECHANISM_NAME, saslClient, getControls(), getResponseTimeoutMillis(connection)); try { return helper.processSASLBind(); } finally { messageID = helper.getMessageID(); } } @Override() public GSSAPIBindRequest getRebindRequest(final String host, final int port) { try { final GSSAPIBindRequestProperties gssapiProperties = new GSSAPIBindRequestProperties(authenticationID, authorizationID, password, realm, kdcAddress, configFilePath); gssapiProperties.setServicePrincipalProtocol(servicePrincipalProtocol); gssapiProperties.setUseTicketCache(useTicketCache); gssapiProperties.setRequireCachedCredentials(requireCachedCredentials); gssapiProperties.setRenewTGT(renewTGT); gssapiProperties.setTicketCachePath(ticketCachePath); gssapiProperties.setEnableGSSAPIDebugging(enableGSSAPIDebugging); return new GSSAPIBindRequest(gssapiProperties, getControls()); } catch (Exception e) { debugException(e); return null; } } @InternalUseOnly() public void handle(final Callback[] callbacks) { for (final Callback callback : callbacks) { if (callback instanceof NameCallback) { ((NameCallback) callback).setName(authenticationID); } else if (callback instanceof PasswordCallback) { if (password == null) { throw new LDAPRuntimeException(new LDAPException( ResultCode.PARAM_ERROR, ERR_GSSAPI_NO_PASSWORD_AVAILABLE.get())); } else { ((PasswordCallback) callback).setPassword( password.stringValue().toCharArray()); } } else if (callback instanceof RealmCallback) { if (realm != null) { ((RealmCallback) callback).setText(realm); } } else { if (debugEnabled(DebugType.LDAP)) { debug(Level.WARNING, DebugType.LDAP, "Unexpected GSSAPI SASL callback of type " + callback.getClass().getName()); } } } } @Override() public int getLastMessageID() { return messageID; } @Override() public GSSAPIBindRequest duplicate() { return duplicate(getControls()); } @Override() public GSSAPIBindRequest duplicate(final Control[] controls) { try { final GSSAPIBindRequestProperties gssapiProperties = new GSSAPIBindRequestProperties(authenticationID, authorizationID, password, realm, kdcAddress, configFilePath); gssapiProperties.setServicePrincipalProtocol(servicePrincipalProtocol); gssapiProperties.setUseTicketCache(useTicketCache); gssapiProperties.setRequireCachedCredentials(requireCachedCredentials); gssapiProperties.setRenewTGT(renewTGT); gssapiProperties.setTicketCachePath(ticketCachePath); gssapiProperties.setEnableGSSAPIDebugging(enableGSSAPIDebugging); final GSSAPIBindRequest bindRequest = new GSSAPIBindRequest(gssapiProperties, controls); bindRequest.setResponseTimeoutMillis(getResponseTimeoutMillis(null)); return bindRequest; } catch (Exception e) { debugException(e); return null; } } @Override() public void toString(final StringBuilder buffer) { buffer.append("GSSAPIBindRequest(authenticationID='"); buffer.append(authenticationID); buffer.append('\''); if (authorizationID != null) { buffer.append(", authorizationID='"); buffer.append(authorizationID); buffer.append('\''); } if (realm != null) { buffer.append(", realm='"); buffer.append(realm); buffer.append('\''); } if (kdcAddress != null) { buffer.append(", kdcAddress='"); buffer.append(kdcAddress); buffer.append('\''); } buffer.append(", configFilePath='"); buffer.append(configFilePath); buffer.append("', servicePrincipalProtocol='"); buffer.append(servicePrincipalProtocol); buffer.append("', enableGSSAPIDebugging="); buffer.append(enableGSSAPIDebugging); final Control[] controls = getControls(); if (controls.length > 0) { buffer.append(", controls={"); for (int i=0; i < controls.length; i++) { if (i > 0) { buffer.append(", "); } buffer.append(controls[i]); } buffer.append('}'); } buffer.append(')'); } }