1 /* 2 * Copyright (C) 2005-2008 Jive Software. All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package org.jivesoftware.openfire.net; 18 19 import java.security.Security; 20 import java.security.cert.Certificate; 21 import java.security.cert.X509Certificate; 22 import java.util.Arrays; 23 import java.util.Collections; 24 import java.util.Enumeration; 25 import java.util.HashMap; 26 import java.util.HashSet; 27 import java.util.Iterator; 28 import java.util.List; 29 import java.util.Map; 30 import java.util.Set; 31 import java.util.regex.Pattern; 32 33 import javax.security.sasl.Sasl; 34 import javax.security.sasl.SaslException; 35 import javax.security.sasl.SaslServer; 36 import javax.security.sasl.SaslServerFactory; 37 38 import org.dom4j.DocumentHelper; 39 import org.dom4j.Element; 40 import org.dom4j.Namespace; 41 import org.dom4j.QName; 42 import org.jivesoftware.openfire.Connection; 43 import org.jivesoftware.openfire.XMPPServer; 44 import org.jivesoftware.openfire.XMPPServerInfo; 45 import org.jivesoftware.openfire.auth.AuthFactory; 46 import org.jivesoftware.openfire.auth.AuthToken; 47 import org.jivesoftware.openfire.keystore.CertificateStoreManager; 48 import org.jivesoftware.openfire.keystore.TrustStore; 49 import org.jivesoftware.openfire.lockout.LockOutManager; 50 import org.jivesoftware.openfire.sasl.AnonymousSaslServer; 51 import org.jivesoftware.openfire.sasl.Failure; 52 import org.jivesoftware.openfire.sasl.JiveSharedSecretSaslServer; 53 import org.jivesoftware.openfire.sasl.SaslFailureException; 54 import org.jivesoftware.openfire.session.ClientSession; 55 import org.jivesoftware.openfire.session.ConnectionSettings; 56 import org.jivesoftware.openfire.session.IncomingServerSession; 57 import org.jivesoftware.openfire.session.LocalClientSession; 58 import org.jivesoftware.openfire.session.LocalIncomingServerSession; 59 import org.jivesoftware.openfire.session.LocalSession; 60 import org.jivesoftware.openfire.session.Session; 61 import org.jivesoftware.openfire.spi.ConnectionType; 62 import org.jivesoftware.util.*; 63 import org.slf4j.Logger; 64 import org.slf4j.LoggerFactory; 65 66 /** 67 * SASLAuthentication is responsible for returning the available SASL mechanisms to use and for 68 * actually performing the SASL authentication.<p> 69 * 70 * The list of available SASL mechanisms is determined by: 71 * <ol> 72 * <li>The type of {@link org.jivesoftware.openfire.user.UserProvider} being used since 73 * some SASL mechanisms require the server to be able to retrieve user passwords</li> 74 * <li>Whether anonymous logins are enabled or not.</li> 75 * <li>Whether shared secret authentication is enabled or not.</li> 76 * <li>Whether the underlying connection has been secured or not.</li> 77 * </ol> 78 * 79 * @author Hao Chen 80 * @author Gaston Dombiak 81 */ 82 public class SASLAuthentication { 83 84 private static final Logger Log = LoggerFactory.getLogger(SASLAuthentication.class); 85 86 public static final SystemProperty<Boolean> SKIP_PEER_CERT_REVALIDATION_CLIENT = SystemProperty.Builder.ofType(Boolean.class) 87 .setKey("xmpp.auth.external.client.skip-cert-revalidation") 88 .setDynamic(true) 89 .setDefaultValue(false) 90 .build(); 91 92 // http://stackoverflow.com/questions/8571501/how-to-check-whether-the-string-is-base64-encoded-or-not 93 // plus an extra regex alternative to catch a single equals sign ('=', see RFC 6120 6.4.2) 94 private static final Pattern BASE64_ENCODED = Pattern.compile("^(=|([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==))$"); 95 96 private static final String SASL_NAMESPACE = "urn:ietf:params:xml:ns:xmpp-sasl"; 97 98 private static Set<String> mechanisms = new HashSet<>(); 99 100 static 101 { 102 // Add (proprietary) Providers of SASL implementation to the Java security context. Security.addProvider( new org.jivesoftware.openfire.sasl.SaslProvider() )103 Security.addProvider( new org.jivesoftware.openfire.sasl.SaslProvider() ); 104 105 // Convert XML based provider setup to Database based 106 JiveGlobals.migrateProperty("sasl.mechs"); 107 JiveGlobals.migrateProperty("sasl.gssapi.debug"); 108 JiveGlobals.migrateProperty("sasl.gssapi.config"); 109 JiveGlobals.migrateProperty("sasl.gssapi.useSubjectCredsOnly"); 110 initMechanisms()111 initMechanisms(); 112 org.jivesoftware.util.PropertyEventDispatcher.addListener( new PropertyEventListener() { @Override public void propertySet( String property, Map<String, Object> params ) { if (R.equals( property ) ) { initMechanisms(); } } @Override public void propertyDeleted( String property, Map<String, Object> params ) { if (R.equals( property ) ) { initMechanisms(); } } @Override public void xmlPropertySet( String property, Map<String, Object> params ) {} @Override public void xmlPropertyDeleted( String property, Map<String, Object> params ) {} } )113 org.jivesoftware.util.PropertyEventDispatcher.addListener( new PropertyEventListener() 114 { 115 @Override 116 public void propertySet( String property, Map<String, Object> params ) 117 { 118 if ("sasl.mechs".equals( property ) ) 119 { 120 initMechanisms(); 121 } 122 } 123 124 @Override 125 public void propertyDeleted( String property, Map<String, Object> params ) 126 { 127 if ("sasl.mechs".equals( property ) ) 128 { 129 initMechanisms(); 130 } 131 } 132 133 @Override 134 public void xmlPropertySet( String property, Map<String, Object> params ) 135 {} 136 137 @Override 138 public void xmlPropertyDeleted( String property, Map<String, Object> params ) 139 {} 140 } ); 141 } 142 143 public enum ElementType 144 { 145 ABORT, 146 AUTH, 147 RESPONSE, 148 CHALLENGE, 149 FAILURE, 150 UNDEF; 151 valueOfCaseInsensitive( String name )152 public static ElementType valueOfCaseInsensitive( String name ) 153 { 154 if ( name == null || name.isEmpty() ) { 155 return UNDEF; 156 } 157 try 158 { 159 return ElementType.valueOf( name.toUpperCase() ); 160 } 161 catch ( Throwable t ) 162 { 163 return UNDEF; 164 } 165 } 166 } 167 168 public enum Status 169 { 170 /** 171 * Entity needs to respond last challenge. Session is still negotiatingSASL authentication. 172 */ 173 needResponse, 174 175 /** 176 * SASL negotiation has failed. The entity may retry a few times before the connection is closed. 177 */ 178 failed, 179 180 /** 181 * SASL negotiation has been successful. 182 */ 183 authenticated 184 } 185 186 /** 187 * Returns a string with the valid SASL mechanisms available for the specified session. If 188 * the session's connection is not secured then only include the SASL mechanisms that don't 189 * require TLS. 190 * 191 * @param session The current session 192 * 193 * @return a string with the valid SASL mechanisms available for the specified session. 194 */ getSASLMechanisms( LocalSession session )195 public static String getSASLMechanisms( LocalSession session ) 196 { 197 if ( session instanceof ClientSession ) 198 { 199 final Element result = getSASLMechanismsElement( (ClientSession) session ); 200 return result == null ? "" : result.asXML(); 201 } 202 else if ( session instanceof LocalIncomingServerSession ) 203 { 204 final Element result = getSASLMechanismsElement( (LocalIncomingServerSession) session ); 205 return result == null ? "" : result.asXML(); 206 } 207 else 208 { 209 Log.debug( "Unable to determine SASL mechanisms that are applicable to session '{}'. Unrecognized session type.", session ); 210 return ""; 211 } 212 } 213 getSASLMechanismsElement( ClientSession session )214 public static Element getSASLMechanismsElement( ClientSession session ) 215 { 216 final Element result = DocumentHelper.createElement( new QName( "mechanisms", new Namespace( "", SASL_NAMESPACE ) ) ); 217 for (String mech : getSupportedMechanisms()) { 218 if (mech.equals("EXTERNAL")) { 219 boolean trustedCert = false; 220 if (session.isSecure()) { 221 final Connection connection = ( (LocalClientSession) session ).getConnection(); 222 if ( SKIP_PEER_CERT_REVALIDATION_CLIENT.getValue() ) { 223 // Trust that the peer certificate has been validated when TLS got established. 224 trustedCert = connection.getPeerCertificates() != null && connection.getPeerCertificates().length > 0; 225 } else { 226 // Re-evaluate the validity of the peer certificate. 227 final TrustStore trustStore = connection.getConfiguration().getTrustStore(); 228 trustedCert = trustStore.isTrusted( connection.getPeerCertificates() ); 229 } 230 } 231 if ( !trustedCert ) { 232 continue; // Do not offer EXTERNAL. 233 } 234 } 235 final Element mechanism = result.addElement("mechanism"); 236 mechanism.setText(mech); 237 } 238 239 // OF-2072: Return null instead of an empty element, if so configured. 240 if ( JiveGlobals.getBooleanProperty("sasl.client.suppressEmpty", false) && result.elements().isEmpty() ) { 241 return null; 242 } 243 244 return result; 245 } 246 getSASLMechanismsElement( LocalIncomingServerSession session )247 public static Element getSASLMechanismsElement( LocalIncomingServerSession session ) 248 { 249 final Element result = DocumentHelper.createElement( new QName( "mechanisms", new Namespace( "", SASL_NAMESPACE ) ) ); 250 if (session.isSecure()) { 251 final Connection connection = session.getConnection(); 252 final TrustStore trustStore = connection.getConfiguration().getTrustStore(); 253 final X509Certificate trusted = trustStore.getEndEntityCertificate( session.getConnection().getPeerCertificates() ); 254 255 boolean haveTrustedCertificate = trusted != null; 256 if (trusted != null && session.getDefaultIdentity() != null) { 257 haveTrustedCertificate = verifyCertificate(trusted, session.getDefaultIdentity()); 258 } 259 if (haveTrustedCertificate) { 260 // Offer SASL EXTERNAL only if TLS has already been negotiated and the peer has a trusted cert. 261 final Element mechanism = result.addElement("mechanism"); 262 mechanism.setText("EXTERNAL"); 263 } 264 } 265 266 // OF-2072: Return null instead of an empty element, if so configured. 267 if ( JiveGlobals.getBooleanProperty("sasl.server.suppressEmpty", false) && result.elements().isEmpty() ) { 268 return null; 269 } 270 return result; 271 } 272 273 /** 274 * Handles the SASL authentication packet. The entity may be sending an initial 275 * authentication request or a response to a challenge made by the server. The returned 276 * value indicates whether the authentication has finished either successfully or not or 277 * if the entity is expected to send a response to a challenge. 278 * 279 * @param session the session that is authenticating with the server. 280 * @param doc the stanza sent by the authenticating entity. 281 * @return value that indicates whether the authentication has finished either successfully 282 * or not or if the entity is expected to send a response to a challenge. 283 */ handle(LocalSession session, Element doc)284 public static Status handle(LocalSession session, Element doc) 285 { 286 try 287 { 288 if ( !doc.getNamespaceURI().equals( SASL_NAMESPACE ) ) 289 { 290 throw new IllegalStateException( "Unexpected data received while negotiating SASL authentication. Name of the offending root element: " + doc.getName() + " Namespace: " + doc.getNamespaceURI() ); 291 } 292 293 switch ( ElementType.valueOfCaseInsensitive( doc.getName() ) ) 294 { 295 case ABORT: 296 throw new SaslFailureException( Failure.ABORTED ); 297 298 case AUTH: 299 if ( doc.attributeValue( "mechanism" ) == null ) 300 { 301 throw new SaslFailureException( Failure.INVALID_MECHANISM, "Peer did not specify a mechanism." ); 302 } 303 304 final String mechanismName = doc.attributeValue( "mechanism" ).toUpperCase(); 305 306 // See if the mechanism is supported by configuration as well as by implementation. 307 if ( !mechanisms.contains( mechanismName ) ) 308 { 309 throw new SaslFailureException( Failure.INVALID_MECHANISM, "The configuration of Openfire does not contain or allow the mechanism." ); 310 } 311 312 // OF-477: The SASL implementation requires the fully qualified host name (not the domain name!) of this server, 313 // yet, most of the XMPP implemenations of DIGEST-MD5 will actually use the domain name. To account for that, 314 // here, we'll use the host name, unless DIGEST-MD5 is being negotiated! 315 final XMPPServerInfo serverInfo = XMPPServer.getInstance().getServerInfo(); 316 final String serverName = ( mechanismName.equals( "DIGEST-MD5" ) ? serverInfo.getXMPPDomain() : serverInfo.getHostname() ); 317 318 // Construct the configuration properties 319 final Map<String, Object> props = new HashMap<>(); 320 props.put( LocalSession.class.getCanonicalName(), session ); 321 props.put(Sasl.POLICY_NOANONYMOUS, Boolean.toString(!AnonymousSaslServer.ENABLED.getValue())); 322 props.put( "com.sun.security.sasl.digest.realm", serverInfo.getXMPPDomain() ); 323 324 SaslServer saslServer = Sasl.createSaslServer( mechanismName, "xmpp", serverName, props, new XMPPCallbackHandler() ); 325 if ( saslServer == null ) 326 { 327 throw new SaslFailureException( Failure.INVALID_MECHANISM, "There is no provider that can provide a SASL server for the desired mechanism and properties." ); 328 } 329 330 session.setSessionData( "SaslServer", saslServer ); 331 332 if ( mechanismName.equals( "DIGEST-MD5" ) ) 333 { 334 // RFC2831 (DIGEST-MD5) says the client MAY provide data in the initial response. Java SASL does 335 // not (currently) support this and throws an exception. For XMPP, such data violates 336 // the RFC, so we just strip any initial token. 337 doc.setText( "" ); 338 } 339 340 // intended fall-through 341 case RESPONSE: 342 343 saslServer = (SaslServer) session.getSessionData( "SaslServer" ); 344 345 if ( saslServer == null ) 346 { 347 // Client sends response without a preceding auth? 348 throw new IllegalStateException( "A SaslServer instance was not initialized and/or stored on the session." ); 349 } 350 351 // Decode any data that is provided in the client response. 352 final String encoded = doc.getTextTrim(); 353 final byte[] decoded; 354 if ( encoded == null || encoded.isEmpty() || encoded.equals("=") ) // java SaslServer cannot handle a null. 355 { 356 decoded = new byte[ 0 ]; 357 } 358 else 359 { 360 // TODO: We shouldn't depend on regex-based validation. Instead, use a proper decoder implementation and handle any exceptions that it throws. 361 if ( !BASE64_ENCODED.matcher( encoded ).matches() ) 362 { 363 throw new SaslFailureException( Failure.INCORRECT_ENCODING ); 364 } 365 366 decoded = StringUtils.decodeBase64( encoded ); 367 } 368 369 // Process client response. 370 final byte[] challenge = saslServer.evaluateResponse( decoded ); // Either a challenge or success data. 371 372 if ( !saslServer.isComplete() ) 373 { 374 // Not complete: client is challenged for additional steps. 375 sendChallenge( session, challenge ); 376 return Status.needResponse; 377 } 378 379 // Success! 380 if ( session instanceof IncomingServerSession ) 381 { 382 // Flag that indicates if certificates of the remote server should be validated. 383 final boolean verify = JiveGlobals.getBooleanProperty( ConnectionSettings.Server.TLS_CERTIFICATE_VERIFY, true ); 384 if ( verify ) 385 { 386 if ( verifyCertificates( session.getConnection().getPeerCertificates(), saslServer.getAuthorizationID(), true ) ) 387 { 388 ( (LocalIncomingServerSession) session ).tlsAuth(); 389 } 390 else 391 { 392 throw new SaslFailureException( Failure.NOT_AUTHORIZED, "Server-to-Server certificate verification failed." ); 393 } 394 } 395 } 396 397 authenticationSuccessful( session, saslServer.getAuthorizationID(), challenge ); 398 session.removeSessionData( "SaslServer" ); 399 return Status.authenticated; 400 401 default: 402 throw new IllegalStateException( "Unexpected data received while negotiating SASL authentication. Name of the offending root element: " + doc.getName() + " Namespace: " + doc.getNamespaceURI() ); 403 } 404 } 405 catch ( SaslException ex ) 406 { 407 Log.debug( "SASL negotiation failed for session: {}", session, ex ); 408 final Failure failure; 409 if ( ex instanceof SaslFailureException && ((SaslFailureException) ex).getFailure() != null ) 410 { 411 failure = ((SaslFailureException) ex).getFailure(); 412 } 413 else 414 { 415 failure = Failure.NOT_AUTHORIZED; 416 } 417 authenticationFailed( session, failure ); 418 session.removeSessionData( "SaslServer" ); 419 return Status.failed; 420 } 421 catch( Exception ex ) 422 { 423 Log.warn( "An unexpected exception occurred during SASL negotiation. Affected session: {}", session, ex ); 424 authenticationFailed( session, Failure.NOT_AUTHORIZED ); 425 session.removeSessionData( "SaslServer" ); 426 return Status.failed; 427 } 428 } 429 verifyCertificate(X509Certificate trustedCert, String hostname)430 public static boolean verifyCertificate(X509Certificate trustedCert, String hostname) { 431 for (String identity : CertificateManager.getServerIdentities(trustedCert)) { 432 // Verify that either the identity is the same as the hostname, or for wildcarded 433 // identities that the hostname ends with .domainspecified or -is- domainspecified. 434 if ((identity.startsWith("*.") 435 && (hostname.endsWith(identity.replace("*.", ".")) 436 || hostname.equals(identity.replace("*.", "")))) 437 || hostname.equals(identity)) { 438 return true; 439 } 440 } 441 return false; 442 } 443 verifyCertificates(Certificate[] chain, String hostname, boolean isS2S)444 public static boolean verifyCertificates(Certificate[] chain, String hostname, boolean isS2S) { 445 final CertificateStoreManager certificateStoreManager = XMPPServer.getInstance().getCertificateStoreManager(); 446 final ConnectionType connectionType = isS2S ? ConnectionType.SOCKET_S2S : ConnectionType.SOCKET_C2S; 447 final TrustStore trustStore = certificateStoreManager.getTrustStore( connectionType ); 448 final X509Certificate trusted = trustStore.getEndEntityCertificate( chain ); 449 if (trusted != null) { 450 return verifyCertificate(trusted, hostname); 451 } 452 return false; 453 } 454 sendElement(Session session, String element, byte[] data)455 private static void sendElement(Session session, String element, byte[] data) { 456 StringBuilder reply = new StringBuilder(250); 457 reply.append("<"); 458 reply.append(element); 459 reply.append(" xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\""); 460 if (data != null) { 461 reply.append(">"); 462 String data_b64 = StringUtils.encodeBase64(data).trim(); 463 if ("".equals(data_b64)) { 464 data_b64 = "="; 465 } 466 reply.append(data_b64); 467 reply.append("</"); 468 reply.append(element); 469 reply.append(">"); 470 } else { 471 reply.append("/>"); 472 } 473 session.deliverRawText(reply.toString()); 474 } 475 sendChallenge(Session session, byte[] challenge)476 private static void sendChallenge(Session session, byte[] challenge) { 477 sendElement(session, "challenge", challenge); 478 } 479 authenticationSuccessful(LocalSession session, String username, byte[] successData)480 private static void authenticationSuccessful(LocalSession session, String username, 481 byte[] successData) { 482 if (username != null && LockOutManager.getInstance().isAccountDisabled(username)) { 483 // Interception! This person is locked out, fail instead! 484 LockOutManager.getInstance().recordFailedLogin(username); 485 authenticationFailed(session, Failure.ACCOUNT_DISABLED); 486 return; 487 } 488 sendElement(session, "success", successData); 489 // We only support SASL for c2s 490 if (session instanceof ClientSession) { 491 final AuthToken authToken; 492 if (username == null) { 493 // AuthzId is null, which indicates that authentication was anonymous. 494 authToken = AuthToken.generateAnonymousToken(); 495 } else { 496 authToken = AuthToken.generateUserToken(username); 497 } 498 ((LocalClientSession) session).setAuthToken(authToken); 499 } 500 else if (session instanceof IncomingServerSession) { 501 String hostname = username; 502 // Add the validated domain as a valid domain. The remote server can 503 // now send packets from this address 504 ((LocalIncomingServerSession) session).addValidatedDomain(hostname); 505 Log.info("Inbound Server {} authenticated (via TLS)", username); 506 } 507 } 508 authenticationFailed(LocalSession session, Failure failure)509 private static void authenticationFailed(LocalSession session, Failure failure) { 510 StringBuilder reply = new StringBuilder(80); 511 reply.append("<failure xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"><"); 512 reply.append(failure.toString()); 513 reply.append("/></failure>"); 514 session.deliverRawText(reply.toString()); 515 // Give a number of retries before closing the connection 516 Integer retries = (Integer) session.getSessionData("authRetries"); 517 if (retries == null) { 518 retries = 1; 519 } 520 else { 521 retries = retries + 1; 522 } 523 session.setSessionData("authRetries", retries); 524 if (retries >= JiveGlobals.getIntProperty("xmpp.auth.retries", 3) ) { 525 // Close the connection 526 Log.debug( "Closing session that failed to authenticate {} times: {}", retries, session ); 527 session.close(); 528 } 529 } 530 531 /** 532 * Adds a new SASL mechanism to the list of supported SASL mechanisms by the server. The 533 * new mechanism will be offered to clients and connection managers as stream features.<p> 534 * 535 * Note: this method simply registers the SASL mechanism to be advertised as a supported 536 * mechanism by Openfire. Actual SASL handling is done by Java itself, so you must add 537 * the provider to Java. 538 * 539 * @param mechanismName the name of the new SASL mechanism (cannot be null or an empty String). 540 */ addSupportedMechanism(String mechanismName)541 public static void addSupportedMechanism(String mechanismName) { 542 if ( mechanismName == null || mechanismName.isEmpty() ) { 543 throw new IllegalArgumentException( "Argument 'mechanism' must cannot be null or an empty string." ); 544 } 545 mechanisms.add( mechanismName.toUpperCase() ); 546 Log.info( "Support added for the '{}' SASL mechanism.", mechanismName.toUpperCase() ); 547 } 548 549 /** 550 * Removes a SASL mechanism from the list of supported SASL mechanisms by the server. 551 * 552 * @param mechanismName the name of the SASL mechanism to remove (cannot be null or empty, not case sensitive). 553 */ removeSupportedMechanism(String mechanismName)554 public static void removeSupportedMechanism(String mechanismName) { 555 if ( mechanismName == null || mechanismName.isEmpty() ) { 556 throw new IllegalArgumentException( "Argument 'mechanism' must cannot be null or an empty string." ); 557 } 558 559 if ( mechanisms.remove( mechanismName.toUpperCase() ) ) 560 { 561 Log.info( "Support removed for the '{}' SASL mechanism.", mechanismName.toUpperCase() ); 562 } 563 } 564 565 /** 566 * Returns the list of supported SASL mechanisms by the server. Note that Java may have 567 * support for more mechanisms but some of them may not be returned since a special setup 568 * is required that might be missing. Use {@link #addSupportedMechanism(String)} to add 569 * new SASL mechanisms. 570 * 571 * @return the list of supported SASL mechanisms by the server. 572 */ getSupportedMechanisms()573 public static Set<String> getSupportedMechanisms() 574 { 575 // List all mechanism names for which there's an implementation. 576 final Set<String> implementedMechanisms = getImplementedMechanisms(); 577 578 // Start off with all mechanisms that we intend to support. 579 final Set<String> answer = new HashSet<>( mechanisms ); 580 581 // Clean up not-available mechanisms. 582 for ( final Iterator<String> it = answer.iterator(); it.hasNext(); ) 583 { 584 final String mechanism = it.next(); 585 586 if ( !implementedMechanisms.contains( mechanism ) ) 587 { 588 Log.trace( "Cannot support '{}' as there's no implementation available.", mechanism ); 589 it.remove(); 590 continue; 591 } 592 593 switch ( mechanism ) 594 { 595 case "CRAM-MD5": // intended fall-through 596 case "DIGEST-MD5": 597 // Check if the user provider in use supports passwords retrieval. Access to the users passwords will be required by the CallbackHandler. 598 if ( !AuthFactory.supportsPasswordRetrieval() ) 599 { 600 Log.trace( "Cannot support '{}' as the AuthFactory that's in use does not support password retrieval.", mechanism ); 601 it.remove(); 602 } 603 break; 604 605 case "SCRAM-SHA-1": 606 if ( !AuthFactory.supportsScram() ) 607 { 608 Log.trace( "Cannot support '{}' as the AuthFactory that's in use does not support SCRAM.", mechanism ); 609 it.remove(); 610 } 611 break; 612 613 case "ANONYMOUS": 614 if (!AnonymousSaslServer.ENABLED.getValue()) { 615 Log.trace( "Cannot support '{}' as it has been disabled by configuration.", mechanism ); 616 it.remove(); 617 } 618 break; 619 620 case "JIVE-SHAREDSECRET": 621 if ( !JiveSharedSecretSaslServer.isSharedSecretAllowed() ) 622 { 623 Log.trace( "Cannot support '{}' as it has been disabled by configuration.", mechanism ); 624 it.remove(); 625 } 626 break; 627 628 case "GSSAPI": 629 final String gssapiConfig = JiveGlobals.getProperty( "sasl.gssapi.config" ); 630 if ( gssapiConfig != null ) 631 { 632 System.setProperty( "java.security.krb5.debug", JiveGlobals.getProperty( "sasl.gssapi.debug", "false" ) ); 633 System.setProperty( "java.security.auth.login.config", gssapiConfig ); 634 System.setProperty( "javax.security.auth.useSubjectCredsOnly", JiveGlobals.getProperty( "sasl.gssapi.useSubjectCredsOnly", "false" ) ); 635 } 636 else 637 { 638 Log.trace( "Cannot support '{}' as the 'sasl.gssapi.config' property has not been defined.", mechanism ); 639 it.remove(); 640 } 641 break; 642 } 643 } 644 return answer; 645 } 646 647 /** 648 * Returns a collection of mechanism names for which the JVM has an implementation available. 649 * <p> 650 * Note that this need not (and likely will not) correspond with the list of mechanisms that is offered to XMPP 651 * peer entities, which is provided by #getSupportedMechanisms. 652 * 653 * @return a collection of SASL mechanism names (never null, possibly empty) 654 */ getImplementedMechanisms()655 public static Set<String> getImplementedMechanisms() 656 { 657 final Set<String> result = new HashSet<>(); 658 final Enumeration<SaslServerFactory> saslServerFactories = Sasl.getSaslServerFactories(); 659 while ( saslServerFactories.hasMoreElements() ) 660 { 661 final SaslServerFactory saslServerFactory = saslServerFactories.nextElement(); 662 Collections.addAll( result, saslServerFactory.getMechanismNames( null ) ); 663 } 664 return result; 665 } 666 667 /** 668 * Returns a collection of SASL mechanism names that forms the source pool from which the mechanisms that are 669 * eventually being offered to peers are obtained. 670 ** 671 * When a mechanism is not returned by this method, it will never be offered, but when a mechanism is returned 672 * by this method, there is no guarantee that it will be offered. 673 * 674 * Apart from being returned in this method, an implementation must be available (see {@link #getImplementedMechanisms()} 675 * and configuration or other characteristics of this server must not prevent a particular mechanism from being 676 * used (see @{link {@link #getSupportedMechanisms()}}. 677 * 678 * @return A collection of mechanisms that are considered for use in this instance of Openfire. 679 */ getEnabledMechanisms()680 public static List<String> getEnabledMechanisms() 681 { 682 return JiveGlobals.getListProperty("sasl.mechs", Arrays.asList( "ANONYMOUS","PLAIN","DIGEST-MD5","CRAM-MD5","SCRAM-SHA-1","JIVE-SHAREDSECRET","GSSAPI","EXTERNAL" ) ); 683 } 684 685 /** 686 * Sets the collection of mechanism names that the system administrator allows to be used. 687 * 688 * @param mechanisms A collection of mechanisms that are considered for use in this instance of Openfire. Null to reset the default setting. 689 * @see #getEnabledMechanisms() 690 */ setEnabledMechanisms( List<String> mechanisms )691 public static void setEnabledMechanisms( List<String> mechanisms ) 692 { 693 JiveGlobals.setProperty( "sasl.mechs", mechanisms ); 694 initMechanisms(); 695 } 696 initMechanisms()697 private static void initMechanisms() 698 { 699 final List<String> propertyValues = getEnabledMechanisms(); 700 mechanisms = new HashSet<>(); 701 for ( final String propertyValue : propertyValues ) 702 { 703 try 704 { 705 addSupportedMechanism( propertyValue ); 706 } 707 catch ( Exception ex ) 708 { 709 Log.warn( "An exception occurred while trying to add support for SASL Mechanism '{}':", propertyValue, ex ); 710 } 711 } 712 } 713 } 714