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}