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