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}