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