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}