| /** |
| * $RCSfile$ |
| * $Revision$ |
| * $Date$ |
| * |
| * Copyright 2003-2007 Jive Software. |
| * |
| * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.jivesoftware.smack; |
| |
| import org.jivesoftware.smack.filter.PacketIDFilter; |
| import org.jivesoftware.smack.packet.Bind; |
| import org.jivesoftware.smack.packet.IQ; |
| import org.jivesoftware.smack.packet.Packet; |
| import org.jivesoftware.smack.packet.Session; |
| import org.jivesoftware.smack.sasl.*; |
| |
| import javax.security.auth.callback.CallbackHandler; |
| import java.io.IOException; |
| import java.lang.reflect.Constructor; |
| import java.util.*; |
| |
| /** |
| * <p>This class is responsible authenticating the user using SASL, binding the resource |
| * to the connection and establishing a session with the server.</p> |
| * |
| * <p>Once TLS has been negotiated (i.e. the connection has been secured) it is possible to |
| * register with the server, authenticate using Non-SASL or authenticate using SASL. If the |
| * server supports SASL then Smack will first try to authenticate using SASL. But if that |
| * fails then Non-SASL will be tried.</p> |
| * |
| * <p>The server may support many SASL mechanisms to use for authenticating. Out of the box |
| * Smack provides several SASL mechanisms, but it is possible to register new SASL Mechanisms. Use |
| * {@link #registerSASLMechanism(String, Class)} to register a new mechanisms. A registered |
| * mechanism wont be used until {@link #supportSASLMechanism(String, int)} is called. By default, |
| * the list of supported SASL mechanisms is determined from the {@link SmackConfiguration}. </p> |
| * |
| * <p>Once the user has been authenticated with SASL, it is necessary to bind a resource for |
| * the connection. If no resource is passed in {@link #authenticate(String, String, String)} |
| * then the server will assign a resource for the connection. In case a resource is passed |
| * then the server will receive the desired resource but may assign a modified resource for |
| * the connection.</p> |
| * |
| * <p>Once a resource has been binded and if the server supports sessions then Smack will establish |
| * a session so that instant messaging and presence functionalities may be used.</p> |
| * |
| * @see org.jivesoftware.smack.sasl.SASLMechanism |
| * |
| * @author Gaston Dombiak |
| * @author Jay Kline |
| */ |
| public class SASLAuthentication implements UserAuthentication { |
| |
| private static Map<String, Class<? extends SASLMechanism>> implementedMechanisms = new HashMap<String, Class<? extends SASLMechanism>>(); |
| private static List<String> mechanismsPreferences = new ArrayList<String>(); |
| |
| private Connection connection; |
| private Collection<String> serverMechanisms = new ArrayList<String>(); |
| private SASLMechanism currentMechanism = null; |
| /** |
| * Boolean indicating if SASL negotiation has finished and was successful. |
| */ |
| private boolean saslNegotiated; |
| /** |
| * Boolean indication if SASL authentication has failed. When failed the server may end |
| * the connection. |
| */ |
| private boolean saslFailed; |
| private boolean resourceBinded; |
| private boolean sessionSupported; |
| /** |
| * The SASL related error condition if there was one provided by the server. |
| */ |
| private String errorCondition; |
| |
| static { |
| |
| // Register SASL mechanisms supported by Smack |
| registerSASLMechanism("EXTERNAL", SASLExternalMechanism.class); |
| registerSASLMechanism("GSSAPI", SASLGSSAPIMechanism.class); |
| registerSASLMechanism("DIGEST-MD5", SASLDigestMD5Mechanism.class); |
| registerSASLMechanism("CRAM-MD5", SASLCramMD5Mechanism.class); |
| registerSASLMechanism("PLAIN", SASLPlainMechanism.class); |
| registerSASLMechanism("ANONYMOUS", SASLAnonymous.class); |
| |
| supportSASLMechanism("GSSAPI",0); |
| supportSASLMechanism("DIGEST-MD5",1); |
| supportSASLMechanism("CRAM-MD5",2); |
| supportSASLMechanism("PLAIN",3); |
| supportSASLMechanism("ANONYMOUS",4); |
| |
| } |
| |
| /** |
| * Registers a new SASL mechanism |
| * |
| * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4. |
| * @param mClass a SASLMechanism subclass. |
| */ |
| public static void registerSASLMechanism(String name, Class<? extends SASLMechanism> mClass) { |
| implementedMechanisms.put(name, mClass); |
| } |
| |
| /** |
| * Unregisters an existing SASL mechanism. Once the mechanism has been unregistered it won't |
| * be possible to authenticate users using the removed SASL mechanism. It also removes the |
| * mechanism from the supported list. |
| * |
| * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4. |
| */ |
| public static void unregisterSASLMechanism(String name) { |
| implementedMechanisms.remove(name); |
| mechanismsPreferences.remove(name); |
| } |
| |
| |
| /** |
| * Registers a new SASL mechanism in the specified preference position. The client will try |
| * to authenticate using the most prefered SASL mechanism that is also supported by the server. |
| * The SASL mechanism must be registered via {@link #registerSASLMechanism(String, Class)} |
| * |
| * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4. |
| */ |
| public static void supportSASLMechanism(String name) { |
| mechanismsPreferences.add(0, name); |
| } |
| |
| /** |
| * Registers a new SASL mechanism in the specified preference position. The client will try |
| * to authenticate using the most prefered SASL mechanism that is also supported by the server. |
| * Use the <tt>index</tt> parameter to set the level of preference of the new SASL mechanism. |
| * A value of 0 means that the mechanism is the most prefered one. The SASL mechanism must be |
| * registered via {@link #registerSASLMechanism(String, Class)} |
| * |
| * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4. |
| * @param index preference position amongst all the implemented SASL mechanism. Starts with 0. |
| */ |
| public static void supportSASLMechanism(String name, int index) { |
| mechanismsPreferences.add(index, name); |
| } |
| |
| /** |
| * Un-supports an existing SASL mechanism. Once the mechanism has been unregistered it won't |
| * be possible to authenticate users using the removed SASL mechanism. Note that the mechanism |
| * is still registered, but will just not be used. |
| * |
| * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4. |
| */ |
| public static void unsupportSASLMechanism(String name) { |
| mechanismsPreferences.remove(name); |
| } |
| |
| /** |
| * Returns the registerd SASLMechanism classes sorted by the level of preference. |
| * |
| * @return the registerd SASLMechanism classes sorted by the level of preference. |
| */ |
| public static List<Class<? extends SASLMechanism>> getRegisterSASLMechanisms() { |
| List<Class<? extends SASLMechanism>> answer = new ArrayList<Class<? extends SASLMechanism>>(); |
| for (String mechanismsPreference : mechanismsPreferences) { |
| answer.add(implementedMechanisms.get(mechanismsPreference)); |
| } |
| return answer; |
| } |
| |
| SASLAuthentication(Connection connection) { |
| super(); |
| this.connection = connection; |
| this.init(); |
| } |
| |
| /** |
| * Returns true if the server offered ANONYMOUS SASL as a way to authenticate users. |
| * |
| * @return true if the server offered ANONYMOUS SASL as a way to authenticate users. |
| */ |
| public boolean hasAnonymousAuthentication() { |
| return serverMechanisms.contains("ANONYMOUS"); |
| } |
| |
| /** |
| * Returns true if the server offered SASL authentication besides ANONYMOUS SASL. |
| * |
| * @return true if the server offered SASL authentication besides ANONYMOUS SASL. |
| */ |
| public boolean hasNonAnonymousAuthentication() { |
| return !serverMechanisms.isEmpty() && (serverMechanisms.size() != 1 || !hasAnonymousAuthentication()); |
| } |
| |
| /** |
| * Performs SASL authentication of the specified user. If SASL authentication was successful |
| * then resource binding and session establishment will be performed. This method will return |
| * the full JID provided by the server while binding a resource to the connection.<p> |
| * |
| * The server may assign a full JID with a username or resource different than the requested |
| * by this method. |
| * |
| * @param username the username that is authenticating with the server. |
| * @param resource the desired resource. |
| * @param cbh the CallbackHandler used to get information from the user |
| * @return the full JID provided by the server while binding a resource to the connection. |
| * @throws XMPPException if an error occures while authenticating. |
| */ |
| public String authenticate(String username, String resource, CallbackHandler cbh) |
| throws XMPPException { |
| // Locate the SASLMechanism to use |
| String selectedMechanism = null; |
| for (String mechanism : mechanismsPreferences) { |
| if (implementedMechanisms.containsKey(mechanism) && |
| serverMechanisms.contains(mechanism)) { |
| selectedMechanism = mechanism; |
| break; |
| } |
| } |
| if (selectedMechanism != null) { |
| // A SASL mechanism was found. Authenticate using the selected mechanism and then |
| // proceed to bind a resource |
| try { |
| Class<? extends SASLMechanism> mechanismClass = implementedMechanisms.get(selectedMechanism); |
| Constructor<? extends SASLMechanism> constructor = mechanismClass.getConstructor(SASLAuthentication.class); |
| currentMechanism = constructor.newInstance(this); |
| // Trigger SASL authentication with the selected mechanism. We use |
| // connection.getHost() since GSAPI requires the FQDN of the server, which |
| // may not match the XMPP domain. |
| currentMechanism.authenticate(username, connection.getHost(), cbh); |
| |
| // Wait until SASL negotiation finishes |
| synchronized (this) { |
| if (!saslNegotiated && !saslFailed) { |
| try { |
| wait(30000); |
| } |
| catch (InterruptedException e) { |
| // Ignore |
| } |
| } |
| } |
| |
| if (saslFailed) { |
| // SASL authentication failed and the server may have closed the connection |
| // so throw an exception |
| if (errorCondition != null) { |
| throw new XMPPException("SASL authentication " + |
| selectedMechanism + " failed: " + errorCondition); |
| } |
| else { |
| throw new XMPPException("SASL authentication failed using mechanism " + |
| selectedMechanism); |
| } |
| } |
| |
| if (saslNegotiated) { |
| // Bind a resource for this connection and |
| return bindResourceAndEstablishSession(resource); |
| } else { |
| // SASL authentication failed |
| } |
| } |
| catch (XMPPException e) { |
| throw e; |
| } |
| catch (Exception e) { |
| e.printStackTrace(); |
| } |
| } |
| else { |
| throw new XMPPException("SASL Authentication failed. No known authentication mechanisims."); |
| } |
| throw new XMPPException("SASL authentication failed"); |
| } |
| |
| /** |
| * Performs SASL authentication of the specified user. If SASL authentication was successful |
| * then resource binding and session establishment will be performed. This method will return |
| * the full JID provided by the server while binding a resource to the connection.<p> |
| * |
| * The server may assign a full JID with a username or resource different than the requested |
| * by this method. |
| * |
| * @param username the username that is authenticating with the server. |
| * @param password the password to send to the server. |
| * @param resource the desired resource. |
| * @return the full JID provided by the server while binding a resource to the connection. |
| * @throws XMPPException if an error occures while authenticating. |
| */ |
| public String authenticate(String username, String password, String resource) |
| throws XMPPException { |
| // Locate the SASLMechanism to use |
| String selectedMechanism = null; |
| for (String mechanism : mechanismsPreferences) { |
| if (implementedMechanisms.containsKey(mechanism) && |
| serverMechanisms.contains(mechanism)) { |
| selectedMechanism = mechanism; |
| break; |
| } |
| } |
| if (selectedMechanism != null) { |
| // A SASL mechanism was found. Authenticate using the selected mechanism and then |
| // proceed to bind a resource |
| try { |
| Class<? extends SASLMechanism> mechanismClass = implementedMechanisms.get(selectedMechanism); |
| Constructor<? extends SASLMechanism> constructor = mechanismClass.getConstructor(SASLAuthentication.class); |
| currentMechanism = constructor.newInstance(this); |
| // Trigger SASL authentication with the selected mechanism. We use |
| // connection.getHost() since GSAPI requires the FQDN of the server, which |
| // may not match the XMPP domain. |
| |
| //The serviceName is basically the value that XMPP server sends to the client as being the location |
| //of the XMPP service we are trying to connect to. This should have the format: host [ "/" serv-name ] |
| //as per RFC-2831 guidelines |
| String serviceName = connection.getServiceName(); |
| currentMechanism.authenticate(username, connection.getHost(), serviceName, password); |
| |
| // Wait until SASL negotiation finishes |
| synchronized (this) { |
| if (!saslNegotiated && !saslFailed) { |
| try { |
| wait(30000); |
| } |
| catch (InterruptedException e) { |
| // Ignore |
| } |
| } |
| } |
| |
| if (saslFailed) { |
| // SASL authentication failed and the server may have closed the connection |
| // so throw an exception |
| if (errorCondition != null) { |
| throw new XMPPException("SASL authentication " + |
| selectedMechanism + " failed: " + errorCondition); |
| } |
| else { |
| throw new XMPPException("SASL authentication failed using mechanism " + |
| selectedMechanism); |
| } |
| } |
| |
| if (saslNegotiated) { |
| // Bind a resource for this connection and |
| return bindResourceAndEstablishSession(resource); |
| } |
| else { |
| // SASL authentication failed so try a Non-SASL authentication |
| return new NonSASLAuthentication(connection) |
| .authenticate(username, password, resource); |
| } |
| } |
| catch (XMPPException e) { |
| throw e; |
| } |
| catch (Exception e) { |
| e.printStackTrace(); |
| // SASL authentication failed so try a Non-SASL authentication |
| return new NonSASLAuthentication(connection) |
| .authenticate(username, password, resource); |
| } |
| } |
| else { |
| // No SASL method was found so try a Non-SASL authentication |
| return new NonSASLAuthentication(connection).authenticate(username, password, resource); |
| } |
| } |
| |
| /** |
| * Performs ANONYMOUS SASL authentication. If SASL authentication was successful |
| * then resource binding and session establishment will be performed. This method will return |
| * the full JID provided by the server while binding a resource to the connection.<p> |
| * |
| * The server will assign a full JID with a randomly generated resource and possibly with |
| * no username. |
| * |
| * @return the full JID provided by the server while binding a resource to the connection. |
| * @throws XMPPException if an error occures while authenticating. |
| */ |
| public String authenticateAnonymously() throws XMPPException { |
| try { |
| currentMechanism = new SASLAnonymous(this); |
| currentMechanism.authenticate(null,null,null,""); |
| |
| // Wait until SASL negotiation finishes |
| synchronized (this) { |
| if (!saslNegotiated && !saslFailed) { |
| try { |
| wait(5000); |
| } |
| catch (InterruptedException e) { |
| // Ignore |
| } |
| } |
| } |
| |
| if (saslFailed) { |
| // SASL authentication failed and the server may have closed the connection |
| // so throw an exception |
| if (errorCondition != null) { |
| throw new XMPPException("SASL authentication failed: " + errorCondition); |
| } |
| else { |
| throw new XMPPException("SASL authentication failed"); |
| } |
| } |
| |
| if (saslNegotiated) { |
| // Bind a resource for this connection and |
| return bindResourceAndEstablishSession(null); |
| } |
| else { |
| return new NonSASLAuthentication(connection).authenticateAnonymously(); |
| } |
| } catch (IOException e) { |
| return new NonSASLAuthentication(connection).authenticateAnonymously(); |
| } |
| } |
| |
| private String bindResourceAndEstablishSession(String resource) throws XMPPException { |
| // Wait until server sends response containing the <bind> element |
| synchronized (this) { |
| if (!resourceBinded) { |
| try { |
| wait(30000); |
| } |
| catch (InterruptedException e) { |
| // Ignore |
| } |
| } |
| } |
| |
| if (!resourceBinded) { |
| // Server never offered resource binding |
| throw new XMPPException("Resource binding not offered by server"); |
| } |
| |
| Bind bindResource = new Bind(); |
| bindResource.setResource(resource); |
| |
| PacketCollector collector = connection |
| .createPacketCollector(new PacketIDFilter(bindResource.getPacketID())); |
| // Send the packet |
| connection.sendPacket(bindResource); |
| // Wait up to a certain number of seconds for a response from the server. |
| Bind response = (Bind) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); |
| collector.cancel(); |
| if (response == null) { |
| throw new XMPPException("No response from the server."); |
| } |
| // If the server replied with an error, throw an exception. |
| else if (response.getType() == IQ.Type.ERROR) { |
| throw new XMPPException(response.getError()); |
| } |
| String userJID = response.getJid(); |
| |
| if (sessionSupported) { |
| Session session = new Session(); |
| collector = connection.createPacketCollector(new PacketIDFilter(session.getPacketID())); |
| // Send the packet |
| connection.sendPacket(session); |
| // Wait up to a certain number of seconds for a response from the server. |
| IQ ack = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); |
| collector.cancel(); |
| if (ack == null) { |
| throw new XMPPException("No response from the server."); |
| } |
| // If the server replied with an error, throw an exception. |
| else if (ack.getType() == IQ.Type.ERROR) { |
| throw new XMPPException(ack.getError()); |
| } |
| } |
| return userJID; |
| } |
| |
| /** |
| * Sets the available SASL mechanism reported by the server. The server will report the |
| * available SASL mechanism once the TLS negotiation was successful. This information is |
| * stored and will be used when doing the authentication for logging in the user. |
| * |
| * @param mechanisms collection of strings with the available SASL mechanism reported |
| * by the server. |
| */ |
| void setAvailableSASLMethods(Collection<String> mechanisms) { |
| this.serverMechanisms = mechanisms; |
| } |
| |
| /** |
| * Returns true if the user was able to authenticate with the server usins SASL. |
| * |
| * @return true if the user was able to authenticate with the server usins SASL. |
| */ |
| public boolean isAuthenticated() { |
| return saslNegotiated; |
| } |
| |
| /** |
| * The server is challenging the SASL authentication we just sent. Forward the challenge |
| * to the current SASLMechanism we are using. The SASLMechanism will send a response to |
| * the server. The length of the challenge-response sequence varies according to the |
| * SASLMechanism in use. |
| * |
| * @param challenge a base64 encoded string representing the challenge. |
| * @throws IOException If a network error occures while authenticating. |
| */ |
| void challengeReceived(String challenge) throws IOException { |
| currentMechanism.challengeReceived(challenge); |
| } |
| |
| /** |
| * Notification message saying that SASL authentication was successful. The next step |
| * would be to bind the resource. |
| */ |
| void authenticated() { |
| synchronized (this) { |
| saslNegotiated = true; |
| // Wake up the thread that is waiting in the #authenticate method |
| notify(); |
| } |
| } |
| |
| /** |
| * Notification message saying that SASL authentication has failed. The server may have |
| * closed the connection depending on the number of possible retries. |
| * |
| * @deprecated replaced by {@see #authenticationFailed(String)}. |
| */ |
| void authenticationFailed() { |
| authenticationFailed(null); |
| } |
| |
| /** |
| * Notification message saying that SASL authentication has failed. The server may have |
| * closed the connection depending on the number of possible retries. |
| * |
| * @param condition the error condition provided by the server. |
| */ |
| void authenticationFailed(String condition) { |
| synchronized (this) { |
| saslFailed = true; |
| errorCondition = condition; |
| // Wake up the thread that is waiting in the #authenticate method |
| notify(); |
| } |
| } |
| |
| /** |
| * Notification message saying that the server requires the client to bind a |
| * resource to the stream. |
| */ |
| void bindingRequired() { |
| synchronized (this) { |
| resourceBinded = true; |
| // Wake up the thread that is waiting in the #authenticate method |
| notify(); |
| } |
| } |
| |
| public void send(Packet stanza) { |
| connection.sendPacket(stanza); |
| } |
| |
| /** |
| * Notification message saying that the server supports sessions. When a server supports |
| * sessions the client needs to send a Session packet after successfully binding a resource |
| * for the session. |
| */ |
| void sessionsSupported() { |
| sessionSupported = true; |
| } |
| |
| /** |
| * Initializes the internal state in order to be able to be reused. The authentication |
| * is used by the connection at the first login and then reused after the connection |
| * is disconnected and then reconnected. |
| */ |
| protected void init() { |
| saslNegotiated = false; |
| saslFailed = false; |
| resourceBinded = false; |
| sessionSupported = false; |
| } |
| } |