Home | History | Annotate | Download | only in smack
      1 /**
      2  * $RCSfile$
      3  * $Revision$
      4  * $Date$
      5  *
      6  * Copyright 2003-2007 Jive Software.
      7  *
      8  * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
      9  * you may not use this file except in compliance with the License.
     10  * You may obtain a copy of the License at
     11  *
     12  *     http://www.apache.org/licenses/LICENSE-2.0
     13  *
     14  * Unless required by applicable law or agreed to in writing, software
     15  * distributed under the License is distributed on an "AS IS" BASIS,
     16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     17  * See the License for the specific language governing permissions and
     18  * limitations under the License.
     19  */
     20 
     21 package org.jivesoftware.smack;
     22 
     23 import org.jivesoftware.smack.filter.PacketIDFilter;
     24 import org.jivesoftware.smack.packet.Bind;
     25 import org.jivesoftware.smack.packet.IQ;
     26 import org.jivesoftware.smack.packet.Packet;
     27 import org.jivesoftware.smack.packet.Session;
     28 import org.jivesoftware.smack.sasl.*;
     29 
     30 import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
     31 import java.io.IOException;
     32 import java.lang.reflect.Constructor;
     33 import java.util.*;
     34 
     35 /**
     36  * <p>This class is responsible authenticating the user using SASL, binding the resource
     37  * to the connection and establishing a session with the server.</p>
     38  *
     39  * <p>Once TLS has been negotiated (i.e. the connection has been secured) it is possible to
     40  * register with the server, authenticate using Non-SASL or authenticate using SASL. If the
     41  * server supports SASL then Smack will first try to authenticate using SASL. But if that
     42  * fails then Non-SASL will be tried.</p>
     43  *
     44  * <p>The server may support many SASL mechanisms to use for authenticating. Out of the box
     45  * Smack provides several SASL mechanisms, but it is possible to register new SASL Mechanisms. Use
     46  * {@link #registerSASLMechanism(String, Class)} to register a new mechanisms. A registered
     47  * mechanism wont be used until {@link #supportSASLMechanism(String, int)} is called. By default,
     48  * the list of supported SASL mechanisms is determined from the {@link SmackConfiguration}. </p>
     49  *
     50  * <p>Once the user has been authenticated with SASL, it is necessary to bind a resource for
     51  * the connection. If no resource is passed in {@link #authenticate(String, String, String)}
     52  * then the server will assign a resource for the connection. In case a resource is passed
     53  * then the server will receive the desired resource but may assign a modified resource for
     54  * the connection.</p>
     55  *
     56  * <p>Once a resource has been binded and if the server supports sessions then Smack will establish
     57  * a session so that instant messaging and presence functionalities may be used.</p>
     58  *
     59  * @see org.jivesoftware.smack.sasl.SASLMechanism
     60  *
     61  * @author Gaston Dombiak
     62  * @author Jay Kline
     63  */
     64 public class SASLAuthentication implements UserAuthentication {
     65 
     66     private static Map<String, Class<? extends SASLMechanism>> implementedMechanisms = new HashMap<String, Class<? extends SASLMechanism>>();
     67     private static List<String> mechanismsPreferences = new ArrayList<String>();
     68 
     69     private Connection connection;
     70     private Collection<String> serverMechanisms = new ArrayList<String>();
     71     private SASLMechanism currentMechanism = null;
     72     /**
     73      * Boolean indicating if SASL negotiation has finished and was successful.
     74      */
     75     private boolean saslNegotiated;
     76     /**
     77      * Boolean indication if SASL authentication has failed. When failed the server may end
     78      * the connection.
     79      */
     80     private boolean saslFailed;
     81     private boolean resourceBinded;
     82     private boolean sessionSupported;
     83     /**
     84      * The SASL related error condition if there was one provided by the server.
     85      */
     86     private String errorCondition;
     87 
     88     static {
     89 
     90         // Register SASL mechanisms supported by Smack
     91         registerSASLMechanism("EXTERNAL", SASLExternalMechanism.class);
     92         registerSASLMechanism("GSSAPI", SASLGSSAPIMechanism.class);
     93         registerSASLMechanism("DIGEST-MD5", SASLDigestMD5Mechanism.class);
     94         registerSASLMechanism("CRAM-MD5", SASLCramMD5Mechanism.class);
     95         registerSASLMechanism("PLAIN", SASLPlainMechanism.class);
     96         registerSASLMechanism("ANONYMOUS", SASLAnonymous.class);
     97 
     98 //        supportSASLMechanism("GSSAPI",0);
     99         supportSASLMechanism("DIGEST-MD5",0);
    100 //        supportSASLMechanism("CRAM-MD5",2);
    101         supportSASLMechanism("PLAIN",1);
    102         supportSASLMechanism("ANONYMOUS",2);
    103 
    104     }
    105 
    106     /**
    107      * Registers a new SASL mechanism
    108      *
    109      * @param name   common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
    110      * @param mClass a SASLMechanism subclass.
    111      */
    112     public static void registerSASLMechanism(String name, Class<? extends SASLMechanism> mClass) {
    113         implementedMechanisms.put(name, mClass);
    114     }
    115 
    116     /**
    117      * Unregisters an existing SASL mechanism. Once the mechanism has been unregistered it won't
    118      * be possible to authenticate users using the removed SASL mechanism. It also removes the
    119      * mechanism from the supported list.
    120      *
    121      * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
    122      */
    123     public static void unregisterSASLMechanism(String name) {
    124         implementedMechanisms.remove(name);
    125         mechanismsPreferences.remove(name);
    126     }
    127 
    128 
    129     /**
    130      * Registers a new SASL mechanism in the specified preference position. The client will try
    131      * to authenticate using the most prefered SASL mechanism that is also supported by the server.
    132      * The SASL mechanism must be registered via {@link #registerSASLMechanism(String, Class)}
    133      *
    134      * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
    135      */
    136     public static void supportSASLMechanism(String name) {
    137         mechanismsPreferences.add(0, name);
    138     }
    139 
    140     /**
    141      * Registers a new SASL mechanism in the specified preference position. The client will try
    142      * to authenticate using the most prefered SASL mechanism that is also supported by the server.
    143      * Use the <tt>index</tt> parameter to set the level of preference of the new SASL mechanism.
    144      * A value of 0 means that the mechanism is the most prefered one. The SASL mechanism must be
    145      * registered via {@link #registerSASLMechanism(String, Class)}
    146      *
    147      * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
    148      * @param index preference position amongst all the implemented SASL mechanism. Starts with 0.
    149      */
    150     public static void supportSASLMechanism(String name, int index) {
    151         mechanismsPreferences.add(index, name);
    152     }
    153 
    154     /**
    155      * Un-supports an existing SASL mechanism. Once the mechanism has been unregistered it won't
    156      * be possible to authenticate users using the removed SASL mechanism. Note that the mechanism
    157      * is still registered, but will just not be used.
    158      *
    159      * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
    160      */
    161     public static void unsupportSASLMechanism(String name) {
    162         mechanismsPreferences.remove(name);
    163     }
    164 
    165     /**
    166      * Returns the registerd SASLMechanism classes sorted by the level of preference.
    167      *
    168      * @return the registerd SASLMechanism classes sorted by the level of preference.
    169      */
    170     public static List<Class<? extends SASLMechanism>> getRegisterSASLMechanisms() {
    171         List<Class<? extends SASLMechanism>> answer = new ArrayList<Class<? extends SASLMechanism>>();
    172         for (String mechanismsPreference : mechanismsPreferences) {
    173             answer.add(implementedMechanisms.get(mechanismsPreference));
    174         }
    175         return answer;
    176     }
    177 
    178     SASLAuthentication(Connection connection) {
    179         super();
    180         this.connection = connection;
    181         this.init();
    182     }
    183 
    184     /**
    185      * Returns true if the server offered ANONYMOUS SASL as a way to authenticate users.
    186      *
    187      * @return true if the server offered ANONYMOUS SASL as a way to authenticate users.
    188      */
    189     public boolean hasAnonymousAuthentication() {
    190         return serverMechanisms.contains("ANONYMOUS");
    191     }
    192 
    193     /**
    194      * Returns true if the server offered SASL authentication besides ANONYMOUS SASL.
    195      *
    196      * @return true if the server offered SASL authentication besides ANONYMOUS SASL.
    197      */
    198     public boolean hasNonAnonymousAuthentication() {
    199         return !serverMechanisms.isEmpty() && (serverMechanisms.size() != 1 || !hasAnonymousAuthentication());
    200     }
    201 
    202     /**
    203      * Performs SASL authentication of the specified user. If SASL authentication was successful
    204      * then resource binding and session establishment will be performed. This method will return
    205      * the full JID provided by the server while binding a resource to the connection.<p>
    206      *
    207      * The server may assign a full JID with a username or resource different than the requested
    208      * by this method.
    209      *
    210      * @param username the username that is authenticating with the server.
    211      * @param resource the desired resource.
    212      * @param cbh the CallbackHandler used to get information from the user
    213      * @return the full JID provided by the server while binding a resource to the connection.
    214      * @throws XMPPException if an error occures while authenticating.
    215      */
    216     public String authenticate(String username, String resource, CallbackHandler cbh)
    217             throws XMPPException {
    218         // Locate the SASLMechanism to use
    219         String selectedMechanism = null;
    220         for (String mechanism : mechanismsPreferences) {
    221             if (implementedMechanisms.containsKey(mechanism) &&
    222                     serverMechanisms.contains(mechanism)) {
    223                 selectedMechanism = mechanism;
    224                 break;
    225             }
    226         }
    227         if (selectedMechanism != null) {
    228             // A SASL mechanism was found. Authenticate using the selected mechanism and then
    229             // proceed to bind a resource
    230             try {
    231                 Class<? extends SASLMechanism> mechanismClass = implementedMechanisms.get(selectedMechanism);
    232                 Constructor<? extends SASLMechanism> constructor = mechanismClass.getConstructor(SASLAuthentication.class);
    233                 currentMechanism = constructor.newInstance(this);
    234                 // Trigger SASL authentication with the selected mechanism. We use
    235                 // connection.getHost() since GSAPI requires the FQDN of the server, which
    236                 // may not match the XMPP domain.
    237                 currentMechanism.authenticate(username, connection.getHost(), cbh);
    238 
    239                 // Wait until SASL negotiation finishes
    240                 synchronized (this) {
    241                     if (!saslNegotiated && !saslFailed) {
    242                         try {
    243                             wait(30000);
    244                         }
    245                         catch (InterruptedException e) {
    246                             // Ignore
    247                         }
    248                     }
    249                 }
    250 
    251                 if (saslFailed) {
    252                     // SASL authentication failed and the server may have closed the connection
    253                     // so throw an exception
    254                     if (errorCondition != null) {
    255                         throw new XMPPException("SASL authentication " +
    256                                 selectedMechanism + " failed: " + errorCondition);
    257                     }
    258                     else {
    259                         throw new XMPPException("SASL authentication failed using mechanism " +
    260                                 selectedMechanism);
    261                     }
    262                 }
    263 
    264                 if (saslNegotiated) {
    265                     // Bind a resource for this connection and
    266                     return bindResourceAndEstablishSession(resource);
    267                 } else {
    268                     // SASL authentication failed
    269                 }
    270             }
    271             catch (XMPPException e) {
    272                 throw e;
    273             }
    274             catch (Exception e) {
    275                 e.printStackTrace();
    276             }
    277         }
    278         else {
    279             throw new XMPPException("SASL Authentication failed. No known authentication mechanisims.");
    280         }
    281         throw new XMPPException("SASL authentication failed");
    282     }
    283 
    284     /**
    285      * Performs SASL authentication of the specified user. If SASL authentication was successful
    286      * then resource binding and session establishment will be performed. This method will return
    287      * the full JID provided by the server while binding a resource to the connection.<p>
    288      *
    289      * The server may assign a full JID with a username or resource different than the requested
    290      * by this method.
    291      *
    292      * @param username the username that is authenticating with the server.
    293      * @param password the password to send to the server.
    294      * @param resource the desired resource.
    295      * @return the full JID provided by the server while binding a resource to the connection.
    296      * @throws XMPPException if an error occures while authenticating.
    297      */
    298     public String authenticate(String username, String password, String resource)
    299             throws XMPPException {
    300         // Locate the SASLMechanism to use
    301         String selectedMechanism = null;
    302         for (String mechanism : mechanismsPreferences) {
    303             if (implementedMechanisms.containsKey(mechanism) &&
    304                     serverMechanisms.contains(mechanism)) {
    305                 selectedMechanism = mechanism;
    306                 break;
    307             }
    308         }
    309         if (selectedMechanism != null) {
    310             // A SASL mechanism was found. Authenticate using the selected mechanism and then
    311             // proceed to bind a resource
    312             try {
    313                 Class<? extends SASLMechanism> mechanismClass = implementedMechanisms.get(selectedMechanism);
    314                 Constructor<? extends SASLMechanism> constructor = mechanismClass.getConstructor(SASLAuthentication.class);
    315                 currentMechanism = constructor.newInstance(this);
    316                 // Trigger SASL authentication with the selected mechanism. We use
    317                 // connection.getHost() since GSAPI requires the FQDN of the server, which
    318                 // may not match the XMPP domain.
    319                 currentMechanism.authenticate(username, connection.getServiceName(), password);
    320 
    321                 // Wait until SASL negotiation finishes
    322                 synchronized (this) {
    323                     if (!saslNegotiated && !saslFailed) {
    324                         try {
    325                             wait(30000);
    326                         }
    327                         catch (InterruptedException e) {
    328                             // Ignore
    329                         }
    330                     }
    331                 }
    332 
    333                 if (saslFailed) {
    334                     // SASL authentication failed and the server may have closed the connection
    335                     // so throw an exception
    336                     if (errorCondition != null) {
    337                         throw new XMPPException("SASL authentication " +
    338                                 selectedMechanism + " failed: " + errorCondition);
    339                     }
    340                     else {
    341                         throw new XMPPException("SASL authentication failed using mechanism " +
    342                                 selectedMechanism);
    343                     }
    344                 }
    345 
    346                 if (saslNegotiated) {
    347                     // Bind a resource for this connection and
    348                     return bindResourceAndEstablishSession(resource);
    349                 }
    350                 else {
    351                     // SASL authentication failed so try a Non-SASL authentication
    352                     return new NonSASLAuthentication(connection)
    353                             .authenticate(username, password, resource);
    354                 }
    355             }
    356             catch (XMPPException e) {
    357                 throw e;
    358             }
    359             catch (Exception e) {
    360                 e.printStackTrace();
    361                 // SASL authentication failed so try a Non-SASL authentication
    362                 return new NonSASLAuthentication(connection)
    363                         .authenticate(username, password, resource);
    364             }
    365         }
    366         else {
    367             // No SASL method was found so try a Non-SASL authentication
    368             return new NonSASLAuthentication(connection).authenticate(username, password, resource);
    369         }
    370     }
    371 
    372     /**
    373      * Performs ANONYMOUS SASL authentication. If SASL authentication was successful
    374      * then resource binding and session establishment will be performed. This method will return
    375      * the full JID provided by the server while binding a resource to the connection.<p>
    376      *
    377      * The server will assign a full JID with a randomly generated resource and possibly with
    378      * no username.
    379      *
    380      * @return the full JID provided by the server while binding a resource to the connection.
    381      * @throws XMPPException if an error occures while authenticating.
    382      */
    383     public String authenticateAnonymously() throws XMPPException {
    384         try {
    385             currentMechanism = new SASLAnonymous(this);
    386             currentMechanism.authenticate(null,null,"");
    387 
    388             // Wait until SASL negotiation finishes
    389             synchronized (this) {
    390                 if (!saslNegotiated && !saslFailed) {
    391                     try {
    392                         wait(5000);
    393                     }
    394                     catch (InterruptedException e) {
    395                         // Ignore
    396                     }
    397                 }
    398             }
    399 
    400             if (saslFailed) {
    401                 // SASL authentication failed and the server may have closed the connection
    402                 // so throw an exception
    403                 if (errorCondition != null) {
    404                     throw new XMPPException("SASL authentication failed: " + errorCondition);
    405                 }
    406                 else {
    407                     throw new XMPPException("SASL authentication failed");
    408                 }
    409             }
    410 
    411             if (saslNegotiated) {
    412                 // Bind a resource for this connection and
    413                 return bindResourceAndEstablishSession(null);
    414             }
    415             else {
    416                 return new NonSASLAuthentication(connection).authenticateAnonymously();
    417             }
    418         } catch (IOException e) {
    419             return new NonSASLAuthentication(connection).authenticateAnonymously();
    420         }
    421     }
    422 
    423     private String bindResourceAndEstablishSession(String resource) throws XMPPException {
    424         // Wait until server sends response containing the <bind> element
    425         synchronized (this) {
    426             if (!resourceBinded) {
    427                 try {
    428                     wait(30000);
    429                 }
    430                 catch (InterruptedException e) {
    431                     // Ignore
    432                 }
    433             }
    434         }
    435 
    436         if (!resourceBinded) {
    437             // Server never offered resource binding
    438             throw new XMPPException("Resource binding not offered by server");
    439         }
    440 
    441         Bind bindResource = new Bind();
    442         bindResource.setResource(resource);
    443 
    444         PacketCollector collector = connection
    445                 .createPacketCollector(new PacketIDFilter(bindResource.getPacketID()));
    446         // Send the packet
    447         connection.sendPacket(bindResource);
    448         // Wait up to a certain number of seconds for a response from the server.
    449         Bind response = (Bind) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
    450         collector.cancel();
    451         if (response == null) {
    452             throw new XMPPException("No response from the server.");
    453         }
    454         // If the server replied with an error, throw an exception.
    455         else if (response.getType() == IQ.Type.ERROR) {
    456             throw new XMPPException(response.getError());
    457         }
    458         String userJID = response.getJid();
    459 
    460         if (sessionSupported) {
    461             Session session = new Session();
    462             collector = connection.createPacketCollector(new PacketIDFilter(session.getPacketID()));
    463             // Send the packet
    464             connection.sendPacket(session);
    465             // Wait up to a certain number of seconds for a response from the server.
    466             IQ ack = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
    467             collector.cancel();
    468             if (ack == null) {
    469                 throw new XMPPException("No response from the server.");
    470             }
    471             // If the server replied with an error, throw an exception.
    472             else if (ack.getType() == IQ.Type.ERROR) {
    473                 throw new XMPPException(ack.getError());
    474             }
    475         }
    476         return userJID;
    477     }
    478 
    479     /**
    480      * Sets the available SASL mechanism reported by the server. The server will report the
    481      * available SASL mechanism once the TLS negotiation was successful. This information is
    482      * stored and will be used when doing the authentication for logging in the user.
    483      *
    484      * @param mechanisms collection of strings with the available SASL mechanism reported
    485      *                   by the server.
    486      */
    487     void setAvailableSASLMethods(Collection<String> mechanisms) {
    488         this.serverMechanisms = mechanisms;
    489     }
    490 
    491     /**
    492      * Returns true if the user was able to authenticate with the server usins SASL.
    493      *
    494      * @return true if the user was able to authenticate with the server usins SASL.
    495      */
    496     public boolean isAuthenticated() {
    497         return saslNegotiated;
    498     }
    499 
    500     /**
    501      * The server is challenging the SASL authentication we just sent. Forward the challenge
    502      * to the current SASLMechanism we are using. The SASLMechanism will send a response to
    503      * the server. The length of the challenge-response sequence varies according to the
    504      * SASLMechanism in use.
    505      *
    506      * @param challenge a base64 encoded string representing the challenge.
    507      * @throws IOException If a network error occures while authenticating.
    508      */
    509     void challengeReceived(String challenge) throws IOException {
    510         currentMechanism.challengeReceived(challenge);
    511     }
    512 
    513     /**
    514      * Notification message saying that SASL authentication was successful. The next step
    515      * would be to bind the resource.
    516      */
    517     void authenticated() {
    518         synchronized (this) {
    519             saslNegotiated = true;
    520             // Wake up the thread that is waiting in the #authenticate method
    521             notify();
    522         }
    523     }
    524 
    525     /**
    526      * Notification message saying that SASL authentication has failed. The server may have
    527      * closed the connection depending on the number of possible retries.
    528      *
    529      * @deprecated replaced by {@see #authenticationFailed(String)}.
    530      */
    531     void authenticationFailed() {
    532         authenticationFailed(null);
    533     }
    534 
    535     /**
    536      * Notification message saying that SASL authentication has failed. The server may have
    537      * closed the connection depending on the number of possible retries.
    538      *
    539      * @param condition the error condition provided by the server.
    540      */
    541     void authenticationFailed(String condition) {
    542         synchronized (this) {
    543             saslFailed = true;
    544             errorCondition = condition;
    545             // Wake up the thread that is waiting in the #authenticate method
    546             notify();
    547         }
    548     }
    549 
    550     /**
    551      * Notification message saying that the server requires the client to bind a
    552      * resource to the stream.
    553      */
    554     void bindingRequired() {
    555         synchronized (this) {
    556             resourceBinded = true;
    557             // Wake up the thread that is waiting in the #authenticate method
    558             notify();
    559         }
    560     }
    561 
    562     public void send(Packet stanza) {
    563         connection.sendPacket(stanza);
    564     }
    565 
    566     /**
    567      * Notification message saying that the server supports sessions. When a server supports
    568      * sessions the client needs to send a Session packet after successfully binding a resource
    569      * for the session.
    570      */
    571     void sessionsSupported() {
    572         sessionSupported = true;
    573     }
    574 
    575     /**
    576      * Initializes the internal state in order to be able to be reused. The authentication
    577      * is used by the connection at the first login and then reused after the connection
    578      * is disconnected and then reconnected.
    579      */
    580     protected void init() {
    581         saslNegotiated = false;
    582         saslFailed = false;
    583         resourceBinded = false;
    584         sessionSupported = false;
    585     }
    586 }
    587