001/**
002 *
003 * Copyright 2003-2007 Jive Software.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.jivesoftware.smack;
019
020import org.jivesoftware.smack.SmackException.NoResponseException;
021import org.jivesoftware.smack.SmackException.NotConnectedException;
022import org.jivesoftware.smack.SmackException.ResourceBindingNotOfferedException;
023import org.jivesoftware.smack.XMPPException.XMPPErrorException;
024import org.jivesoftware.smack.packet.Packet;
025import org.jivesoftware.smack.sasl.SASLAnonymous;
026import org.jivesoftware.smack.sasl.SASLCramMD5Mechanism;
027import org.jivesoftware.smack.sasl.SASLDigestMD5Mechanism;
028import org.jivesoftware.smack.sasl.SASLErrorException;
029import org.jivesoftware.smack.sasl.SASLExternalMechanism;
030import org.jivesoftware.smack.sasl.SASLGSSAPIMechanism;
031import org.jivesoftware.smack.sasl.SASLMechanism;
032import org.jivesoftware.smack.sasl.SASLMechanism.SASLFailure;
033import org.jivesoftware.smack.sasl.SASLPlainMechanism;
034
035import javax.security.auth.callback.CallbackHandler;
036import javax.security.sasl.SaslException;
037
038import java.io.IOException;
039import java.lang.reflect.Constructor;
040import java.util.ArrayList;
041import java.util.Collection;
042import java.util.HashMap;
043import java.util.List;
044import java.util.Map;
045
046/**
047 * <p>This class is responsible authenticating the user using SASL, binding the resource
048 * to the connection and establishing a session with the server.</p>
049 *
050 * <p>Once TLS has been negotiated (i.e. the connection has been secured) it is possible to
051 * register with the server, authenticate using Non-SASL or authenticate using SASL. If the
052 * server supports SASL then Smack will first try to authenticate using SASL. But if that
053 * fails then Non-SASL will be tried.</p>
054 *
055 * <p>The server may support many SASL mechanisms to use for authenticating. Out of the box
056 * Smack provides several SASL mechanisms, but it is possible to register new SASL Mechanisms. Use
057 * {@link #registerSASLMechanism(String, Class)} to register a new mechanisms. A registered
058 * mechanism wont be used until {@link #supportSASLMechanism(String, int)} is called. By default,
059 * the list of supported SASL mechanisms is determined from the {@link SmackConfiguration}. </p>
060 *
061 * <p>Once the user has been authenticated with SASL, it is necessary to bind a resource for
062 * the connection. If no resource is passed in {@link #authenticate(String, String, String)}
063 * then the server will assign a resource for the connection. In case a resource is passed
064 * then the server will receive the desired resource but may assign a modified resource for
065 * the connection.</p>
066 *
067 * <p>Once a resource has been binded and if the server supports sessions then Smack will establish
068 * a session so that instant messaging and presence functionalities may be used.</p>
069 *
070 * @see org.jivesoftware.smack.sasl.SASLMechanism
071 *
072 * @author Gaston Dombiak
073 * @author Jay Kline
074 */
075public class SASLAuthentication {
076
077    private static Map<String, Class<? extends SASLMechanism>> implementedMechanisms = new HashMap<String, Class<? extends SASLMechanism>>();
078    private static List<String> mechanismsPreferences = new ArrayList<String>();
079
080    private XMPPConnection connection;
081    private Collection<String> serverMechanisms = new ArrayList<String>();
082    private SASLMechanism currentMechanism = null;
083    /**
084     * Boolean indicating if SASL negotiation has finished and was successful.
085     */
086    private boolean saslNegotiated;
087
088    /**
089     * The SASL related error condition if there was one provided by the server.
090     */
091    private SASLFailure saslFailure;
092
093    static {
094
095        // Register SASL mechanisms supported by Smack
096        registerSASLMechanism("EXTERNAL", SASLExternalMechanism.class);
097        registerSASLMechanism("GSSAPI", SASLGSSAPIMechanism.class);
098        registerSASLMechanism("DIGEST-MD5", SASLDigestMD5Mechanism.class);
099        registerSASLMechanism("CRAM-MD5", SASLCramMD5Mechanism.class);
100        registerSASLMechanism("PLAIN", SASLPlainMechanism.class);
101        registerSASLMechanism("ANONYMOUS", SASLAnonymous.class);
102
103        supportSASLMechanism("GSSAPI",0);
104        supportSASLMechanism("DIGEST-MD5",1);
105        supportSASLMechanism("CRAM-MD5",2);
106        supportSASLMechanism("PLAIN",3);
107        supportSASLMechanism("ANONYMOUS",4);
108
109    }
110
111    /**
112     * Registers a new SASL mechanism
113     *
114     * @param name   common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
115     * @param mClass a SASLMechanism subclass.
116     */
117    public static void registerSASLMechanism(String name, Class<? extends SASLMechanism> mClass) {
118        implementedMechanisms.put(name, mClass);
119    }
120
121    /**
122     * Unregisters an existing SASL mechanism. Once the mechanism has been unregistered it won't
123     * be possible to authenticate users using the removed SASL mechanism. It also removes the
124     * mechanism from the supported list.
125     *
126     * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
127     */
128    public static void unregisterSASLMechanism(String name) {
129        implementedMechanisms.remove(name);
130        mechanismsPreferences.remove(name);
131    }
132
133
134    /**
135     * Registers a new SASL mechanism in the specified preference position. The client will try
136     * to authenticate using the most prefered SASL mechanism that is also supported by the server.
137     * The SASL mechanism must be registered via {@link #registerSASLMechanism(String, Class)}
138     *
139     * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
140     */
141    public static void supportSASLMechanism(String name) {
142        mechanismsPreferences.add(0, name);
143    }
144
145    /**
146     * Registers a new SASL mechanism in the specified preference position. The client will try
147     * to authenticate using the most prefered SASL mechanism that is also supported by the server.
148     * Use the <tt>index</tt> parameter to set the level of preference of the new SASL mechanism.
149     * A value of 0 means that the mechanism is the most prefered one. The SASL mechanism must be
150     * registered via {@link #registerSASLMechanism(String, Class)}
151     *
152     * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
153     * @param index preference position amongst all the implemented SASL mechanism. Starts with 0.
154     */
155    public static void supportSASLMechanism(String name, int index) {
156        mechanismsPreferences.add(index, name);
157    }
158
159    /**
160     * Un-supports an existing SASL mechanism. Once the mechanism has been unregistered it won't
161     * be possible to authenticate users using the removed SASL mechanism. Note that the mechanism
162     * is still registered, but will just not be used.
163     *
164     * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
165     */
166    public static void unsupportSASLMechanism(String name) {
167        mechanismsPreferences.remove(name);
168    }
169
170    /**
171     * Returns the registerd SASLMechanism classes sorted by the level of preference.
172     *
173     * @return the registerd SASLMechanism classes sorted by the level of preference.
174     */
175    public static List<Class<? extends SASLMechanism>> getRegisterSASLMechanisms() {
176        List<Class<? extends SASLMechanism>> answer = new ArrayList<Class<? extends SASLMechanism>>();
177        for (String mechanismsPreference : mechanismsPreferences) {
178            answer.add(implementedMechanisms.get(mechanismsPreference));
179        }
180        return answer;
181    }
182
183    SASLAuthentication(XMPPConnection connection) {
184        super();
185        this.connection = connection;
186        this.init();
187    }
188
189    /**
190     * Returns true if the server offered ANONYMOUS SASL as a way to authenticate users.
191     *
192     * @return true if the server offered ANONYMOUS SASL as a way to authenticate users.
193     */
194    public boolean hasAnonymousAuthentication() {
195        return serverMechanisms.contains("ANONYMOUS");
196    }
197
198    /**
199     * Returns true if the server offered SASL authentication besides ANONYMOUS SASL.
200     *
201     * @return true if the server offered SASL authentication besides ANONYMOUS SASL.
202     */
203    public boolean hasNonAnonymousAuthentication() {
204        return !serverMechanisms.isEmpty() && (serverMechanisms.size() != 1 || !hasAnonymousAuthentication());
205    }
206
207    /**
208     * Performs SASL authentication of the specified user. If SASL authentication was successful
209     * then resource binding and session establishment will be performed. This method will return
210     * the full JID provided by the server while binding a resource to the connection.<p>
211     *
212     * The server may assign a full JID with a username or resource different than the requested
213     * by this method.
214     *
215     * @param resource the desired resource.
216     * @param cbh the CallbackHandler used to get information from the user
217     * @throws IOException 
218     * @throws XMPPErrorException 
219     * @throws NoResponseException 
220     * @throws SASLErrorException 
221     * @throws ResourceBindingNotOfferedException
222     * @throws NotConnectedException 
223     */
224    public void authenticate(String resource, CallbackHandler cbh) throws IOException,
225                    NoResponseException, XMPPErrorException, SASLErrorException, ResourceBindingNotOfferedException, NotConnectedException {
226        // Locate the SASLMechanism to use
227        String selectedMechanism = null;
228        for (String mechanism : mechanismsPreferences) {
229            if (implementedMechanisms.containsKey(mechanism)
230                            && serverMechanisms.contains(mechanism)) {
231                selectedMechanism = mechanism;
232                break;
233            }
234        }
235        if (selectedMechanism != null) {
236            // A SASL mechanism was found. Authenticate using the selected mechanism and then
237            // proceed to bind a resource
238            Class<? extends SASLMechanism> mechanismClass = implementedMechanisms.get(selectedMechanism);
239
240            Constructor<? extends SASLMechanism> constructor;
241            try {
242                constructor = mechanismClass.getConstructor(SASLAuthentication.class);
243                currentMechanism = constructor.newInstance(this);
244            }
245            catch (Exception e) {
246                throw new SaslException("Exception when creating the SASLAuthentication instance",
247                                e);
248            }
249
250            synchronized (this) {
251                // Trigger SASL authentication with the selected mechanism. We use
252                // connection.getHost() since GSAPI requires the FQDN of the server, which
253                // may not match the XMPP domain.
254                currentMechanism.authenticate(connection.getHost(), cbh);
255                try {
256                    // Wait until SASL negotiation finishes
257                    wait(connection.getPacketReplyTimeout());
258                }
259                catch (InterruptedException e) {
260                    // Ignore
261                }
262            }
263
264            if (saslFailure != null) {
265                // SASL authentication failed and the server may have closed the connection
266                // so throw an exception
267                throw new SASLErrorException(selectedMechanism, saslFailure);
268            }
269
270            if (!saslNegotiated) {
271                throw new NoResponseException();
272            }
273        }
274        else {
275            throw new SaslException(
276                            "SASL Authentication failed. No known authentication mechanisims.");
277        }
278    }
279
280    /**
281     * Performs SASL authentication of the specified user. If SASL authentication was successful
282     * then resource binding and session establishment will be performed. This method will return
283     * the full JID provided by the server while binding a resource to the connection.<p>
284     *
285     * The server may assign a full JID with a username or resource different than the requested
286     * by this method.
287     *
288     * @param username the username that is authenticating with the server.
289     * @param password the password to send to the server.
290     * @param resource the desired resource.
291     * @throws XMPPErrorException 
292     * @throws SASLErrorException 
293     * @throws IOException 
294     * @throws SaslException 
295     * @throws SmackException 
296     */
297    public void authenticate(String username, String password, String resource)
298                    throws XMPPErrorException, SASLErrorException, SaslException, IOException,
299                    SmackException {
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            Class<? extends SASLMechanism> mechanismClass = implementedMechanisms.get(selectedMechanism);
313            try {
314                Constructor<? extends SASLMechanism> constructor = mechanismClass.getConstructor(SASLAuthentication.class);
315                currentMechanism = constructor.newInstance(this);
316            }
317            catch (Exception e) {
318                throw new SaslException("Exception when creating the SASLAuthentication instance",
319                                e);
320            }
321
322            synchronized (this) {
323                // Trigger SASL authentication with the selected mechanism. We use
324                // connection.getHost() since GSAPI requires the FQDN of the server, which
325                // may not match the XMPP domain.
326
327                // The serviceName is basically the value that XMPP server sends to the client as
328                // being
329                // the location of the XMPP service we are trying to connect to. This should have
330                // the
331                // format: host ["/" serv-name ] as per RFC-2831 guidelines
332                String serviceName = connection.getServiceName();
333                currentMechanism.authenticate(username, connection.getHost(), serviceName, password);
334
335                try {
336                    // Wait until SASL negotiation finishes
337                    wait(connection.getPacketReplyTimeout());
338                }
339                catch (InterruptedException e) {
340                    // Ignore
341                }
342
343            }
344
345            if (saslFailure != null) {
346                // SASL authentication failed and the server may have closed the connection
347                // so throw an exception
348                throw new SASLErrorException(selectedMechanism, saslFailure);
349            }
350
351            if (!saslNegotiated) {
352                throw new NoResponseException();
353            }
354        }
355        else {
356            throw new SaslException(
357                            "SASL Authentication failed. No known authentication mechanisims.");
358        }
359    }
360
361    /**
362     * Performs ANONYMOUS SASL authentication. If SASL authentication was successful
363     * then resource binding and session establishment will be performed. This method will return
364     * the full JID provided by the server while binding a resource to the connection.<p>
365     *
366     * The server will assign a full JID with a randomly generated resource and possibly with
367     * no username.
368     *
369     * @throws SASLErrorException 
370     * @throws IOException 
371     * @throws SaslException 
372     * @throws XMPPErrorException if an error occures while authenticating.
373     * @throws SmackException if there was no response from the server.
374     */
375    public void authenticateAnonymously() throws SASLErrorException, SaslException, IOException,
376                    SmackException, XMPPErrorException {
377        currentMechanism = new SASLAnonymous(this);
378
379        // Wait until SASL negotiation finishes
380        synchronized (this) {
381            currentMechanism.authenticate(null, null, null, "");
382            try {
383                wait(connection.getPacketReplyTimeout());
384            }
385            catch (InterruptedException e) {
386                // Ignore
387            }
388        }
389
390        if (saslFailure != null) {
391            // SASL authentication failed and the server may have closed the connection
392            // so throw an exception
393            throw new SASLErrorException(currentMechanism.toString(), saslFailure);
394        }
395
396        if (!saslNegotiated) {
397            throw new NoResponseException();
398        }
399    }
400
401    /**
402     * Sets the available SASL mechanism reported by the server. The server will report the
403     * available SASL mechanism once the TLS negotiation was successful. This information is
404     * stored and will be used when doing the authentication for logging in the user.
405     *
406     * @param mechanisms collection of strings with the available SASL mechanism reported
407     *                   by the server.
408     */
409    public void setAvailableSASLMethods(Collection<String> mechanisms) {
410        this.serverMechanisms = mechanisms;
411    }
412
413    /**
414     * Returns true if the user was able to authenticate with the server usins SASL.
415     *
416     * @return true if the user was able to authenticate with the server usins SASL.
417     */
418    public boolean isAuthenticated() {
419        return saslNegotiated;
420    }
421
422    /**
423     * The server is challenging the SASL authentication we just sent. Forward the challenge
424     * to the current SASLMechanism we are using. The SASLMechanism will send a response to
425     * the server. The length of the challenge-response sequence varies according to the
426     * SASLMechanism in use.
427     *
428     * @param challenge a base64 encoded string representing the challenge.
429     * @throws IOException If a network error occures while authenticating.
430     * @throws NotConnectedException 
431     */
432    public void challengeReceived(String challenge) throws IOException, NotConnectedException {
433        currentMechanism.challengeReceived(challenge);
434    }
435
436    /**
437     * Notification message saying that SASL authentication was successful. The next step
438     * would be to bind the resource.
439     */
440    public void authenticated() {
441        saslNegotiated = true;
442        // Wake up the thread that is waiting in the #authenticate method
443        synchronized (this) {
444            notify();
445        }
446    }
447
448    /**
449     * Notification message saying that SASL authentication has failed. The server may have
450     * closed the connection depending on the number of possible retries.
451     * 
452     * @param saslFailure the SASL failure as reported by the server
453     * @see <a href="https://tools.ietf.org/html/rfc6120#section-6.5">RFC6120 6.5</a>
454     */
455    public void authenticationFailed(SASLFailure saslFailure) {
456        this.saslFailure = saslFailure;
457        // Wake up the thread that is waiting in the #authenticate method
458        synchronized (this) {
459            notify();
460        }
461    }
462
463    public void send(Packet stanza) throws NotConnectedException {
464        connection.sendPacket(stanza);
465    }
466
467    
468    /**
469     * Initializes the internal state in order to be able to be reused. The authentication
470     * is used by the connection at the first login and then reused after the connection
471     * is disconnected and then reconnected.
472     */
473    protected void init() {
474        saslNegotiated = false;
475        saslFailure = null;
476    }
477}