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