001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2006-2009 Sun Microsystems, Inc.
015 * Portions Copyright 2011-2016 ForgeRock AS.
016 */
017package org.opends.server.extensions;
018
019import static org.opends.messages.ExtensionMessages.*;
020import static org.opends.server.config.ConfigConstants.*;
021import static org.opends.server.util.ServerConstants.*;
022import static org.opends.server.util.StaticUtils.*;
023
024import java.io.BufferedWriter;
025import java.io.File;
026import java.io.FileWriter;
027import java.io.IOException;
028import java.net.InetAddress;
029import java.net.UnknownHostException;
030import java.util.HashMap;
031import java.util.List;
032
033import javax.security.auth.callback.Callback;
034import javax.security.auth.callback.CallbackHandler;
035import javax.security.auth.callback.UnsupportedCallbackException;
036import javax.security.auth.login.LoginContext;
037import javax.security.auth.login.LoginException;
038import javax.security.sasl.Sasl;
039import javax.security.sasl.SaslException;
040
041import org.forgerock.i18n.LocalizableMessage;
042import org.forgerock.i18n.LocalizableMessageBuilder;
043import org.forgerock.i18n.slf4j.LocalizedLogger;
044import org.forgerock.opendj.config.server.ConfigException;
045import org.forgerock.opendj.ldap.ResultCode;
046import org.ietf.jgss.GSSException;
047import org.forgerock.opendj.config.server.ConfigurationChangeListener;
048import org.forgerock.opendj.server.config.meta.GSSAPISASLMechanismHandlerCfgDefn.QualityOfProtection;
049import org.forgerock.opendj.server.config.server.GSSAPISASLMechanismHandlerCfg;
050import org.forgerock.opendj.server.config.server.SASLMechanismHandlerCfg;
051import org.opends.server.api.ClientConnection;
052import org.opends.server.api.IdentityMapper;
053import org.opends.server.api.SASLMechanismHandler;
054import org.opends.server.core.BindOperation;
055import org.opends.server.core.DirectoryServer;
056import org.forgerock.opendj.config.server.ConfigChangeResult;
057import org.forgerock.opendj.ldap.DN;
058import org.opends.server.types.InitializationException;
059
060/**
061 * This class provides an implementation of a SASL mechanism that
062 * authenticates clients through Kerberos v5 over GSSAPI.
063 */
064public class GSSAPISASLMechanismHandler extends
065    SASLMechanismHandler<GSSAPISASLMechanismHandlerCfg> implements
066    ConfigurationChangeListener<GSSAPISASLMechanismHandlerCfg>, CallbackHandler
067{
068  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
069
070  /** The DN of the configuration entry for this SASL mechanism handler. */
071  private DN configEntryDN;
072
073  /** The current configuration for this SASL mechanism handler. */
074  private GSSAPISASLMechanismHandlerCfg configuration;
075
076  /** The identity mapper that will be used to map identities. */
077  private IdentityMapper<?> identityMapper;
078
079  /** The properties to use when creating a SASL server to process the GSSAPI authentication. */
080  private HashMap<String, String> saslProps;
081
082  /** The fully qualified domain name used when creating the SASL server. */
083  private String serverFQDN;
084
085  /** The login context used to perform server-side authentication. */
086  private volatile LoginContext loginContext;
087  private final Object loginContextLock = new Object();
088
089  /**
090   * Creates a new instance of this SASL mechanism handler. No
091   * initialization should be done in this method, as it should all be
092   * performed in the <CODE>initializeSASLMechanismHandler</CODE>
093   * method.
094   */
095  public GSSAPISASLMechanismHandler()
096  {
097    super();
098  }
099
100  @Override
101  public void initializeSASLMechanismHandler(
102      GSSAPISASLMechanismHandlerCfg configuration) throws ConfigException,
103      InitializationException {
104    try {
105      initialize(configuration);
106      DirectoryServer.registerSASLMechanismHandler(SASL_MECHANISM_GSSAPI, this);
107      configuration.addGSSAPIChangeListener(this);
108      this.configuration = configuration;
109      logger.error(INFO_GSSAPI_STARTED);
110    }
111    catch (UnknownHostException unhe)
112    {
113      logger.traceException(unhe);
114      LocalizableMessage message = ERR_SASL_CANNOT_GET_SERVER_FQDN.get(configEntryDN, getExceptionMessage(unhe));
115      throw new InitializationException(message, unhe);
116    }
117    catch (IOException ioe)
118    {
119      logger.traceException(ioe);
120      LocalizableMessage message = ERR_SASLGSSAPI_CANNOT_CREATE_JAAS_CONFIG
121          .get(getExceptionMessage(ioe));
122      throw new InitializationException(message, ioe);
123    }
124  }
125
126  /**
127   * Checks to make sure that the ds-cfg-kdc-address and dc-cfg-realm
128   * are both defined in the configuration. If only one is set, then
129   * that is an error. If both are defined, or, both are null that is
130   * fine.
131   *
132   * @param configuration
133   *          The configuration to use.
134   * @throws InitializationException
135   *           If the properties violate the requirements.
136   */
137  private void getKdcRealm(GSSAPISASLMechanismHandlerCfg configuration)
138      throws InitializationException
139  {
140    String kdcAddress = configuration.getKdcAddress();
141    String realm = configuration.getRealm();
142    if ((kdcAddress != null && realm == null)
143        || (kdcAddress == null && realm != null))
144    {
145      LocalizableMessage message = ERR_SASLGSSAPI_KDC_REALM_NOT_DEFINED.get();
146      throw new InitializationException(message);
147    }
148    else if (kdcAddress != null)
149    {
150      System.setProperty(KRBV_PROPERTY_KDC, kdcAddress);
151      System.setProperty(KRBV_PROPERTY_REALM, realm);
152    }
153  }
154
155  /**
156   * During login, callbacks are usually used to prompt for passwords.
157   * All of the GSSAPI login information is provided in the properties
158   * and login.conf file, so callbacks are ignored.
159   *
160   * @param callbacks
161   *          An array of callbacks to process.
162   * @throws UnsupportedCallbackException
163   *           if an error occurs.
164   */
165  @Override
166  public void handle(Callback[] callbacks) throws UnsupportedCallbackException
167  {
168  }
169
170  /**
171   * Returns the fully qualified name either defined in the
172   * configuration, or, determined by examining the system
173   * configuration.
174   *
175   * @param configuration
176   *          The configuration to check.
177   * @return The fully qualified hostname of the server.
178   * @throws UnknownHostException
179   *           If the name cannot be determined from the system
180   *           configuration.
181   */
182  private String getFQDN(GSSAPISASLMechanismHandlerCfg configuration)
183      throws UnknownHostException
184  {
185    String serverName = configuration.getServerFqdn();
186    if (serverName == null)
187    {
188      serverName = InetAddress.getLocalHost().getCanonicalHostName();
189    }
190    return serverName;
191  }
192
193  /**
194   * Return the login context. If it's not been initialized yet,
195   * create a login context or login using the principal and keytab
196   * information specified in the configuration.
197   *
198   * @return the login context
199   * @throws LoginException
200   *           If a login context cannot be created.
201   */
202  private LoginContext getLoginContext() throws LoginException
203  {
204    if (loginContext == null)
205    {
206      synchronized (loginContextLock)
207      {
208        if (loginContext == null)
209        {
210          loginContext = new LoginContext(
211                GSSAPISASLMechanismHandler.class.getName(), this);
212          loginContext.login();
213        }
214      }
215    }
216    return loginContext;
217  }
218
219  /** Logout of the current login context. */
220  private void logout()
221  {
222    try
223    {
224      synchronized (loginContextLock)
225      {
226        if (loginContext != null)
227        {
228          loginContext.logout();
229          loginContext = null;
230        }
231      }
232    }
233    catch (LoginException e)
234    {
235      logger.traceException(e);
236    }
237  }
238
239  /**
240   * Creates an login.conf file from information in the specified
241   * configuration. This file is used during the login phase.
242   *
243   * @param configuration
244   *          The new configuration to use.
245   * @return The filename of the new configuration file.
246   * @throws IOException
247   *           If the configuration file cannot be created.
248   */
249  private String configureLoginConfFile(
250      GSSAPISASLMechanismHandlerCfg configuration)
251  throws IOException, InitializationException {
252    File tempFile = File.createTempFile("login", ".conf",
253        getFileForPath(CONFIG_DIR_NAME));
254    String configFileName = tempFile.getAbsolutePath();
255    tempFile.deleteOnExit();
256    BufferedWriter w = new BufferedWriter(new FileWriter(tempFile, false));
257    w.write(getClass().getName() + " {");
258    w.newLine();
259    w.write("  com.sun.security.auth.module.Krb5LoginModule required "
260        + "storeKey=true useKeyTab=true doNotPrompt=true ");
261    String keyTabFilePath = configuration.getKeytab();
262    if(keyTabFilePath == null) {
263      String home = System.getProperty("user.home");
264      String sep = System.getProperty("file.separator");
265      keyTabFilePath = home+sep+"krb5.keytab";
266    }
267    File keyTabFile = new File(keyTabFilePath);
268    if(!keyTabFile.exists()) {
269      LocalizableMessage msg = ERR_SASL_GSSAPI_KEYTAB_INVALID.get(keyTabFilePath);
270      throw new InitializationException(msg);
271    }
272    w.write("keyTab=\"" + keyTabFile + "\" ");
273    StringBuilder principal = new StringBuilder();
274    String principalName = configuration.getPrincipalName();
275    String realm = configuration.getRealm();
276    if (principalName != null)
277    {
278      principal.append("principal=\"").append(principalName);
279    }
280    else
281    {
282      principal.append("principal=\"ldap/").append(serverFQDN);
283    }
284    if (realm != null)
285    {
286      principal.append("@").append(realm);
287    }
288    w.write(principal.toString());
289    logger.error(INFO_GSSAPI_PRINCIPAL_NAME, principal);
290    w.write("\" isInitiator=false;");
291    w.newLine();
292    w.write("};");
293    w.newLine();
294    w.flush();
295    w.close();
296    return configFileName;
297  }
298
299  @Override
300  public void finalizeSASLMechanismHandler() {
301    logout();
302    if(configuration != null)
303    {
304      configuration.removeGSSAPIChangeListener(this);
305    }
306    DirectoryServer.deregisterSASLMechanismHandler(SASL_MECHANISM_GSSAPI);
307    clearProperties();
308    logger.error(INFO_GSSAPI_STOPPED);
309  }
310
311private void clearProperties() {
312  System.clearProperty(KRBV_PROPERTY_KDC);
313  System.clearProperty(KRBV_PROPERTY_REALM);
314  System.clearProperty(JAAS_PROPERTY_CONFIG_FILE);
315  System.clearProperty(JAAS_PROPERTY_SUBJECT_CREDS_ONLY);
316}
317
318  @Override
319  public void processSASLBind(BindOperation bindOp)
320  {
321    ClientConnection connection = bindOp.getClientConnection();
322    if (connection == null)
323    {
324      LocalizableMessage message = ERR_SASLGSSAPI_NO_CLIENT_CONNECTION.get();
325      bindOp.setAuthFailureReason(message);
326      bindOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
327      return;
328    }
329    SASLContext saslContext = (SASLContext) connection.getSASLAuthStateInfo();
330    if (saslContext == null) {
331      try {
332        saslContext = SASLContext.createSASLContext(saslProps, serverFQDN,
333                                  SASL_MECHANISM_GSSAPI, identityMapper);
334      } catch (SaslException ex) {
335        logger.traceException(ex);
336        LocalizableMessage msg;
337        GSSException gex = (GSSException) ex.getCause();
338        if(gex != null) {
339          msg = ERR_SASL_CONTEXT_CREATE_ERROR.get(SASL_MECHANISM_GSSAPI,
340              getGSSExceptionMessage(gex));
341        } else {
342          msg = ERR_SASL_CONTEXT_CREATE_ERROR.get(SASL_MECHANISM_GSSAPI,
343              getExceptionMessage(ex));
344        }
345        connection.setSASLAuthStateInfo(null);
346        bindOp.setAuthFailureReason(msg);
347        bindOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
348        return;
349      }
350    }
351    try
352    {
353      saslContext.performAuthentication(getLoginContext(), bindOp);
354    }
355    catch (LoginException ex)
356    {
357      logger.traceException(ex);
358      LocalizableMessage message = ERR_SASLGSSAPI_CANNOT_CREATE_LOGIN_CONTEXT
359            .get(getExceptionMessage(ex));
360      // Log a configuration error.
361      logger.error(message);
362      connection.setSASLAuthStateInfo(null);
363      bindOp.setAuthFailureReason(message);
364      bindOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
365    }
366  }
367
368  /**
369   * Get the underlying GSSException messages that really tell what the
370   * problem is. The major code is the GSS-API status and the minor is the
371   * mechanism specific error.
372   *
373   * @param gex The GSSException thrown.
374   *
375   * @return The message containing the major and (optional) minor codes and
376   *         strings.
377   */
378  public static LocalizableMessage getGSSExceptionMessage(GSSException gex) {
379    LocalizableMessageBuilder message = new LocalizableMessageBuilder();
380    message.append("major code (").append(gex.getMajor()).append(") ")
381        .append(gex.getMajorString());
382    if(gex.getMinor() != 0)
383    {
384      message.append(", minor code (").append(gex.getMinor()).append(") ")
385          .append(gex.getMinorString());
386    }
387    return message.toMessage();
388  }
389
390  @Override
391  public boolean isPasswordBased(String mechanism)
392  {
393    // This is not a password-based mechanism.
394    return false;
395  }
396
397  @Override
398  public boolean isSecure(String mechanism)
399  {
400    // This may be considered a secure mechanism.
401    return true;
402  }
403
404  @Override
405  public boolean isConfigurationAcceptable(
406      SASLMechanismHandlerCfg configuration, List<LocalizableMessage> unacceptableReasons)
407  {
408    GSSAPISASLMechanismHandlerCfg newConfig =
409      (GSSAPISASLMechanismHandlerCfg) configuration;
410    return isConfigurationChangeAcceptable(newConfig, unacceptableReasons);
411  }
412
413  @Override
414  public boolean isConfigurationChangeAcceptable(
415      GSSAPISASLMechanismHandlerCfg newConfiguration,
416      List<LocalizableMessage> unacceptableReasons) {
417    boolean isAcceptable = true;
418
419    try
420    {
421      getFQDN(newConfiguration);
422    }
423    catch (UnknownHostException ex)
424    {
425      logger.traceException(ex);
426      unacceptableReasons.add(ERR_SASL_CANNOT_GET_SERVER_FQDN.get(
427          configEntryDN, getExceptionMessage(ex)));
428      isAcceptable = false;
429    }
430
431    String keyTabFilePath = newConfiguration.getKeytab();
432    if(keyTabFilePath == null) {
433      String home = System.getProperty("user.home");
434      String sep = System.getProperty("file.separator");
435      keyTabFilePath = home+sep+"krb5.keytab";
436    }
437    File keyTabFile = new File(keyTabFilePath);
438    if(!keyTabFile.exists()) {
439      LocalizableMessage message = ERR_SASL_GSSAPI_KEYTAB_INVALID.get(keyTabFilePath);
440      unacceptableReasons.add(message);
441      logger.trace(message);
442      isAcceptable = false;
443    }
444
445    String kdcAddress = newConfiguration.getKdcAddress();
446    String realm = newConfiguration.getRealm();
447    if ((kdcAddress != null && realm == null)
448        || (kdcAddress == null && realm != null))
449    {
450      LocalizableMessage message = ERR_SASLGSSAPI_KDC_REALM_NOT_DEFINED.get();
451      unacceptableReasons.add(message);
452      logger.trace(message);
453      isAcceptable = false;
454    }
455
456    return isAcceptable;
457  }
458
459  @Override
460  public ConfigChangeResult applyConfigurationChange(GSSAPISASLMechanismHandlerCfg newConfiguration)
461  {
462    final ConfigChangeResult ccr = new ConfigChangeResult();
463    try
464    {
465      logout();
466      clearProperties();
467      initialize(newConfiguration);
468      this.configuration = newConfiguration;
469    }
470    catch (InitializationException ex) {
471      logger.traceException(ex);
472      ccr.addMessage(ex.getMessageObject());
473      clearProperties();
474      ccr.setResultCode(ResultCode.OTHER);
475    } catch (UnknownHostException ex) {
476      logger.traceException(ex);
477      ccr.addMessage(ERR_SASL_CANNOT_GET_SERVER_FQDN.get(configEntryDN, getExceptionMessage(ex)));
478      clearProperties();
479      ccr.setResultCode(ResultCode.OTHER);
480    } catch (IOException ex) {
481      logger.traceException(ex);
482      ccr.addMessage(ERR_SASLGSSAPI_CANNOT_CREATE_JAAS_CONFIG.get(getExceptionMessage(ex)));
483      clearProperties();
484      ccr.setResultCode(ResultCode.OTHER);
485    }
486    return ccr;
487  }
488
489/**
490 * Try to initialize the GSSAPI mechanism handler with the specified config.
491 *
492 * @param config The configuration to use.
493 *
494 * @throws UnknownHostException
495 *      If a host name does not resolve.
496 * @throws IOException
497 *      If there was a problem creating the login file.
498 * @throws InitializationException
499 *      If the keytab file does not exist.
500 */
501private void initialize(GSSAPISASLMechanismHandlerCfg config)
502throws UnknownHostException, IOException, InitializationException
503{
504    configEntryDN = config.dn();
505    DN identityMapperDN = config.getIdentityMapperDN();
506    identityMapper = DirectoryServer.getIdentityMapper(identityMapperDN);
507    serverFQDN = getFQDN(config);
508    logger.error(INFO_GSSAPI_SERVER_FQDN, serverFQDN);
509    saslProps = new HashMap<>();
510    saslProps.put(Sasl.QOP, getQOP(config));
511    saslProps.put(Sasl.REUSE, "false");
512    String configFileName = configureLoginConfFile(config);
513    System.setProperty(JAAS_PROPERTY_CONFIG_FILE, configFileName);
514    System.setProperty(JAAS_PROPERTY_SUBJECT_CREDS_ONLY, "false");
515    getKdcRealm(config);
516}
517
518  /**
519   * Retrieves the QOP (quality-of-protection) from the specified
520   * configuration.
521   *
522   * @param configuration
523   *          The new configuration to use.
524   * @return A string representing the quality-of-protection.
525   */
526  private String getQOP(GSSAPISASLMechanismHandlerCfg configuration)
527  {
528    QualityOfProtection QOP = configuration.getQualityOfProtection();
529    if (QOP.equals(QualityOfProtection.CONFIDENTIALITY)) {
530      return "auth-conf";
531    } else if (QOP.equals(QualityOfProtection.INTEGRITY)) {
532      return "auth-int";
533    } else {
534      return "auth";
535    }
536  }
537}