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 */
017package org.jivesoftware.smack.sasl;
018
019import org.jivesoftware.smack.SASLAuthentication;
020import org.jivesoftware.smack.SmackException.NotConnectedException;
021import org.jivesoftware.smack.packet.Packet;
022import org.jivesoftware.smack.util.StringUtils;
023
024import java.io.IOException;
025import java.util.Map;
026import java.util.HashMap;
027
028import javax.security.auth.callback.CallbackHandler;
029import javax.security.auth.callback.UnsupportedCallbackException;
030import javax.security.auth.callback.Callback;
031import javax.security.auth.callback.NameCallback;
032import javax.security.auth.callback.PasswordCallback;
033import javax.security.sasl.RealmCallback;
034import javax.security.sasl.RealmChoiceCallback;
035import javax.security.sasl.Sasl;
036import javax.security.sasl.SaslClient;
037import javax.security.sasl.SaslException;
038
039/**
040 * Base class for SASL mechanisms. Subclasses must implement these methods:
041 * <ul>
042 *  <li>{@link #getName()} -- returns the common name of the SASL mechanism.</li>
043 * </ul>
044 * Subclasses will likely want to implement their own versions of these methods:
045 *  <li>{@link #authenticate(String, String, String, String)} -- Initiate authentication stanza using the
046 *  deprecated method.</li>
047 *  <li>{@link #authenticate(String, CallbackHandler)} -- Initiate authentication stanza
048 *  using the CallbackHandler method.</li>
049 *  <li>{@link #challengeReceived(String)} -- Handle a challenge from the server.</li>
050 * </ul>
051 * 
052 * Basic XMPP SASL authentication steps:
053 * 1. Client authentication initialization, stanza sent to the server (Base64 encoded): 
054 *    <auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='DIGEST-MD5'/>
055 * 2. Server sends back to the client the challenge response (Base64 encoded)
056 *    sample: 
057 *    realm=<sasl server realm>,nonce="OA6MG9tEQGm2hh",qop="auth",charset=utf-8,algorithm=md5-sess
058 * 3. The client responds back to the server (Base 64 encoded):
059 *    sample:
060 *    username=<userid>,realm=<sasl server realm from above>,nonce="OA6MG9tEQGm2hh",
061 *    cnonce="OA6MHXh6VqTrRk",nc=00000001,qop=auth,
062 *    digest-uri=<digesturi>,
063 *    response=d388dad90d4bbd760a152321f2143af7,
064 *    charset=utf-8,
065 *    authzid=<id>
066 * 4. The server evaluates if the user is present and contained in the REALM
067 *    if successful it sends: <response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/> (Base64 encoded)
068 *    if not successful it sends:
069 *    sample:
070 *    <challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
071 *        cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZmZmZA==
072 *    </challenge>
073 *
074 * @author Jay Kline
075 */
076public abstract class SASLMechanism implements CallbackHandler {
077
078    private SASLAuthentication saslAuthentication;
079    protected SaslClient sc;
080    protected String authenticationId;
081    protected String password;
082    protected String hostname;
083
084    public SASLMechanism(SASLAuthentication saslAuthentication) {
085        this.saslAuthentication = saslAuthentication;
086    }
087
088    /**
089     * Builds and sends the <tt>auth</tt> stanza to the server. Note that this method of
090     * authentication is not recommended, since it is very inflexable. Use
091     * {@link #authenticate(String, CallbackHandler)} whenever possible.
092     * 
093     * Explanation of auth stanza:
094     * 
095     * The client authentication stanza needs to include the digest-uri of the form: xmpp/serverName 
096     * From RFC-2831: 
097     * digest-uri = "digest-uri" "=" digest-uri-value
098     * digest-uri-value = serv-type "/" host [ "/" serv-name ]
099     * 
100     * digest-uri: 
101     * Indicates the principal name of the service with which the client 
102     * wishes to connect, formed from the serv-type, host, and serv-name. 
103     * For example, the FTP service
104     * on "ftp.example.com" would have a "digest-uri" value of "ftp/ftp.example.com"; the SMTP
105     * server from the example above would have a "digest-uri" value of
106     * "smtp/mail3.example.com/example.com".
107     * 
108     * host:
109     * The DNS host name or IP address for the service requested. The DNS host name
110     * must be the fully-qualified canonical name of the host. The DNS host name is the
111     * preferred form; see notes on server processing of the digest-uri.
112     * 
113     * serv-name: 
114     * Indicates the name of the service if it is replicated. The service is
115     * considered to be replicated if the client's service-location process involves resolution
116     * using standard DNS lookup operations, and if these operations involve DNS records (such
117     * as SRV, or MX) which resolve one DNS name into a set of other DNS names. In this case,
118     * the initial name used by the client is the "serv-name", and the final name is the "host"
119     * component. For example, the incoming mail service for "example.com" may be replicated
120     * through the use of MX records stored in the DNS, one of which points at an SMTP server
121     * called "mail3.example.com"; it's "serv-name" would be "example.com", it's "host" would be
122     * "mail3.example.com". If the service is not replicated, or the serv-name is identical to
123     * the host, then the serv-name component MUST be omitted
124     * 
125     * digest-uri verification is needed for ejabberd 2.0.3 and higher   
126     * 
127     * @param username the username of the user being authenticated.
128     * @param host the hostname where the user account resides.
129     * @param serviceName the xmpp service location - used by the SASL client in digest-uri creation
130     * serviceName format is: host [ "/" serv-name ] as per RFC-2831
131     * @param password the password for this account.
132     * @throws IOException If a network error occurs while authenticating.
133     * @throws SaslException
134     * @throws NotConnectedException 
135     */
136    public void authenticate(String username, String host, String serviceName, String password) throws IOException, SaslException, NotConnectedException {
137        //Since we were not provided with a CallbackHandler, we will use our own with the given
138        //information
139
140        //Set the authenticationID as the username, since they must be the same in this case.
141        this.authenticationId = username;
142        this.password = password;
143        this.hostname = host;
144
145        String[] mechanisms = { getName() };
146        Map<String,String> props = new HashMap<String,String>();
147        sc = Sasl.createSaslClient(mechanisms, null, "xmpp", serviceName, props, this);
148        authenticate();
149    }
150
151    /**
152     * Builds and sends the <tt>auth</tt> stanza to the server. The callback handler will handle
153     * any additional information, such as the authentication ID or realm, if it is needed.
154     *
155     * @param host     the hostname where the user account resides.
156     * @param cbh      the CallbackHandler to obtain user information.
157     * @throws IOException If a network error occures while authenticating.
158     * @throws SaslException If a protocol error occurs or the user is not authenticated.
159     * @throws NotConnectedException 
160     */
161    public void authenticate(String host, CallbackHandler cbh) throws IOException, SaslException, NotConnectedException {
162        String[] mechanisms = { getName() };
163        Map<String,String> props = new HashMap<String,String>();
164        sc = Sasl.createSaslClient(mechanisms, null, "xmpp", host, props, cbh);
165        authenticate();
166    }
167
168    protected void authenticate() throws IOException, SaslException, NotConnectedException {
169        String authenticationText = null;
170        if (sc.hasInitialResponse()) {
171            byte[] response = sc.evaluateChallenge(new byte[0]);
172            authenticationText = StringUtils.encodeBase64(response, false);
173        }
174
175        // Send the authentication to the server
176        getSASLAuthentication().send(new AuthMechanism(getName(), authenticationText));
177    }
178
179    /**
180     * The server is challenging the SASL mechanism for the stanza he just sent. Send a
181     * response to the server's challenge.
182     *
183     * @param challenge a base64 encoded string representing the challenge.
184     * @throws IOException if an exception sending the response occurs.
185     * @throws NotConnectedException 
186     */
187    public void challengeReceived(String challenge) throws IOException, NotConnectedException {
188        byte response[];
189        if(challenge != null) {
190            response = sc.evaluateChallenge(StringUtils.decodeBase64(challenge));
191        } else {
192            response = sc.evaluateChallenge(new byte[0]);
193        }
194
195        Packet responseStanza;
196        if (response == null) {
197            responseStanza = new Response();
198        }
199        else {
200            responseStanza = new Response(StringUtils.encodeBase64(response, false));
201        }
202
203        // Send the authentication to the server
204        getSASLAuthentication().send(responseStanza);
205    }
206
207    /**
208     * Returns the common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or GSSAPI.
209     *
210     * @return the common name of the SASL mechanism.
211     */
212    protected abstract String getName();
213
214    protected SASLAuthentication getSASLAuthentication() {
215        return saslAuthentication;
216    }
217
218    /**
219     * 
220     */
221    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
222        for (int i = 0; i < callbacks.length; i++) {
223            if (callbacks[i] instanceof NameCallback) {
224                NameCallback ncb = (NameCallback)callbacks[i];
225                ncb.setName(authenticationId);
226            } else if(callbacks[i] instanceof PasswordCallback) {
227                PasswordCallback pcb = (PasswordCallback)callbacks[i];
228                pcb.setPassword(password.toCharArray());
229            } else if(callbacks[i] instanceof RealmCallback) {
230                RealmCallback rcb = (RealmCallback)callbacks[i];
231                //Retrieve the REALM from the challenge response that the server returned when the client initiated the authentication 
232                //exchange. If this value is not null or empty, *this value* has to be sent back to the server in the client's response 
233                //to the server's challenge
234                String text = rcb.getDefaultText();
235                //The SASL client (sc) created in smack  uses rcb.getText when creating the negotiatedRealm to send it back to the server
236                //Make sure that this value matches the server's realm
237                rcb.setText(text);
238            } else if(callbacks[i] instanceof RealmChoiceCallback){
239                //unused
240                //RealmChoiceCallback rccb = (RealmChoiceCallback)callbacks[i];
241            } else {
242               throw new UnsupportedCallbackException(callbacks[i]);
243            }
244         }
245    }
246
247    /**
248     * Initiating SASL authentication by select a mechanism.
249     */
250    public static class AuthMechanism extends Packet {
251        final private String name;
252        final private String authenticationText;
253
254        public AuthMechanism(String name, String authenticationText) {
255            if (name == null) {
256                throw new NullPointerException("SASL mechanism name shouldn't be null.");
257            }
258            this.name = name;
259            this.authenticationText = authenticationText;
260        }
261
262        public String toXML() {
263            StringBuilder stanza = new StringBuilder();
264            stanza.append("<auth mechanism=\"").append(name);
265            stanza.append("\" xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
266            if (authenticationText != null &&
267                    authenticationText.trim().length() > 0) {
268                stanza.append(authenticationText);
269            }
270            stanza.append("</auth>");
271            return stanza.toString();
272        }
273    }
274
275    /**
276     * A SASL challenge stanza.
277     */
278    public static class Challenge extends Packet {
279        final private String data;
280
281        public Challenge(String data) {
282            this.data = data;
283        }
284
285        public String toXML() {
286            StringBuilder stanza = new StringBuilder();
287            stanza.append("<challenge xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
288            if (data != null &&
289                    data.trim().length() > 0) {
290                stanza.append(data);
291            }
292            stanza.append("</challenge>");
293            return stanza.toString();
294        }
295    }
296
297    /**
298     * A SASL response stanza.
299     */
300    public static class Response extends Packet {
301        final private String authenticationText;
302
303        public Response() {
304            authenticationText = null;
305        }
306
307        public Response(String authenticationText) {
308            if (authenticationText == null || authenticationText.trim().length() == 0) {
309                this.authenticationText = null;
310            }
311            else {
312                this.authenticationText = authenticationText;
313            }
314        }
315
316        public String toXML() {
317            StringBuilder stanza = new StringBuilder();
318            stanza.append("<response xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
319            if (authenticationText != null) {
320                stanza.append(authenticationText);
321            }
322            stanza.append("</response>");
323            return stanza.toString();
324        }
325    }
326
327    /**
328     * A SASL success stanza.
329     */
330    public static class Success extends Packet {
331        final private String data;
332
333        public Success(String data) {
334            this.data = data;
335        }
336
337        public String toXML() {
338            StringBuilder stanza = new StringBuilder();
339            stanza.append("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
340            if (data != null &&
341                    data.trim().length() > 0) {
342                stanza.append(data);
343            }
344            stanza.append("</success>");
345            return stanza.toString();
346        }
347    }
348
349    /**
350     * A SASL failure stanza.
351     */
352    public static class SASLFailure extends Packet {
353        private final SASLError saslError;
354        private final String saslErrorString;
355
356        public SASLFailure(String saslError) {
357            SASLError error = SASLError.fromString(saslError);
358            if (error == null) {
359                // RFC6120 6.5 states that unknown condition must be treat as generic authentication failure.
360                this.saslError = SASLError.not_authorized;
361            } else {
362                this.saslError = error;
363            }
364            this.saslErrorString = saslError;
365        }
366
367        /**
368         * Get the SASL related error condition.
369         * 
370         * @return the SASL related error condition.
371         */
372        public SASLError getSASLError() {
373            return saslError;
374        }
375
376        /**
377         * 
378         * @return the SASL error as String
379         */
380        public String getSASLErrorString() {
381            return saslErrorString;
382        }
383
384        public String toXML() {
385            StringBuilder stanza = new StringBuilder();
386            stanza.append("<failure xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
387            stanza.append("<").append(saslErrorString).append("/>");
388            stanza.append("</failure>");
389            return stanza.toString();
390        }
391    }
392}