SASLAuthentication.java 22.6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14
/**
 * $RCSfile$
 * $Revision: $
 * $Date: $
 *
 * Copyright (C) 2005 Jive Software. All rights reserved.
 *
 * This software is published under the terms of the GNU Public License (GPL),
 * a copy of which is included in this distribution.
 */

package org.jivesoftware.wildfire.net;

import org.dom4j.Element;
15
import org.jivesoftware.util.JiveGlobals;
16 17
import org.jivesoftware.util.Log;
import org.jivesoftware.util.StringUtils;
18 19 20 21 22 23
import org.jivesoftware.wildfire.ClientSession;
import org.jivesoftware.wildfire.Session;
import org.jivesoftware.wildfire.XMPPServer;
import org.jivesoftware.wildfire.auth.AuthFactory;
import org.jivesoftware.wildfire.auth.AuthToken;
import org.jivesoftware.wildfire.auth.UnauthorizedException;
24
import org.jivesoftware.wildfire.server.IncomingServerSession;
25 26
import org.xmpp.packet.JID;

27
import javax.net.ssl.SSLPeerUnverifiedException;
28 29 30
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;
31
import java.io.UnsupportedEncodingException;
32 33
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
34
import java.util.*;
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58

/**
 * SASLAuthentication is responsible for returning the available SASL mechanisms to use and for
 * actually performing the SASL authentication.<p>
 *
 * The list of available SASL mechanisms is determined by 1) the type of
 * {@link org.jivesoftware.wildfire.user.UserProvider} being used since some SASL mechanisms
 * require the server to be able to retrieve user passwords; 2) whether anonymous logins are
 * enabled or not and 3) whether the underlying connection has been secured or not.
 *
 * @author Hao Chen
 * @author Gaston Dombiak
 */
public class SASLAuthentication {

    /**
     * The utf-8 charset for decoding and encoding Jabber packet streams.
     */
    protected static String CHARSET = "UTF-8";

    private static final String SASL_NAMESPACE = "xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"";

    private static Map<String, ElementType> typeMap = new TreeMap<String, ElementType>();

59 60 61 62 63
    private static Set<String> mechanisms = null;

    static {
        initMechanisms();
    }
64

65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
    public enum ElementType {

        AUTH("auth"), RESPONSE("response"), CHALLENGE("challenge"), FAILURE("failure"), UNDEF("");

        private String name = null;

        public String toString() {
            return name;
        }

        private ElementType(String name) {
            this.name = name;
            typeMap.put(this.name, this);
        }

        public static ElementType valueof(String name) {
            if (name == null) {
                return UNDEF;
            }
            ElementType e = typeMap.get(name);
            return e != null ? e : UNDEF;
        }
    }

89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
    public enum Status {
        /**
         * Entity needs to respond last challenge. Session is still negotiating
         * SASL authentication.
         */
        needResponse,
        /**
         * SASL negotiation has failed. The entity may retry a few times before the connection
         * is closed.
         */
        failed,
        /**
         * SASL negotiation has been successful.
         */
        authenticated
104 105 106 107 108 109 110 111 112 113
    }

    /**
     * Returns a string with the valid SASL mechanisms available for the specified session. If
     * the session's connection is not secured then only include the SASL mechanisms that don't
     * require TLS.
     *
     * @return a string with the valid SASL mechanisms available for the specified session.
     */
    public static String getSASLMechanisms(Session session) {
114 115 116
        if (!(session instanceof ClientSession) && !(session instanceof IncomingServerSession)) {
            return "";
        }
117 118 119
        StringBuilder sb = new StringBuilder(195);
        sb.append("<mechanisms xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
        if (session.getConnection().isSecure() && session instanceof IncomingServerSession) {
120
            // Server connections dont follow the same rules as clients
121 122
            sb.append("<mechanism>EXTERNAL</mechanism>");
        }
123
        else {
124 125 126 127
            for (String mech : getSupportedMechanisms()) {
                if (mech.equals("CRAM-MD5") || mech.equals("DIGEST-MD5")) {
                    // Check if the user provider in use supports passwords retrieval. Accessing
                    // to the users passwords will be required by the CallbackHandler
128
                    if (!AuthFactory.getAuthProvider().supportsPasswordRetrieval()) {
129 130 131 132 133
                        continue;
                    }
                }
                else if (mech.equals("ANONYMOUS")) {
                    // Check anonymous is supported
134
                    if (!XMPPServer.getInstance().getIQAuthHandler().isAnonymousAllowed()) {
135 136 137 138 139 140
                        continue;
                    }
                }
                sb.append("<mechanism>");
                sb.append(mech);
                sb.append("</mechanism>");
141 142
            }
        }
143 144 145 146
        sb.append("</mechanisms>");
        return sb.toString();
    }

147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
    /**
     * Handles the SASL authentication packet. The entity may be sending an initial
     * authentication request or a response to a challenge made by the server. The returned
     * value indicates whether the authentication has finished either successfully or not or
     * if the entity is expected to send a response to a challenge.
     *
     * @param session the session that is authenticating with the server.
     * @param doc the stanza sent by the authenticating entity.
     * @return value that indicates whether the authentication has finished either successfully
     *         or not or if the entity is expected to send a response to a challenge.
     * @throws UnsupportedEncodingException If UTF-8 charset is not supported.
     */
    public static Status handle(Session session, Element doc) throws UnsupportedEncodingException {
        Status status;
        String mechanism;
        if (doc.getNamespace().asXML().equals(SASL_NAMESPACE)) {
            ElementType type = ElementType.valueof(doc.getName());
            switch (type) {
                case AUTH:
                    mechanism = doc.attributeValue("mechanism");
                    // Store the requested SASL mechanism by the client
                    session.setSessionData("SaslMechanism", mechanism);
                    //Log.debug("SASLAuthentication.doHandshake() AUTH entered: "+mechanism);
                    if (mechanism.equalsIgnoreCase("PLAIN") &&
                            getSupportedMechanisms().contains("PLAIN")) {
                        status = doPlainAuthentication(session, doc);
                    }
                    else if (mechanism.equalsIgnoreCase("ANONYMOUS") &&
                            getSupportedMechanisms().contains("ANONYMOUS")) {
                        status = doAnonymousAuthentication(session);
                    }
                    else if (mechanism.equalsIgnoreCase("EXTERNAL")) {
                        status = doExternalAuthentication(session, doc);
                    }
                    else if (getSupportedMechanisms().contains(mechanism)) {
                        // The selected SASL mechanism requires the server to send a challenge
                        // to the client
                        try {
                            Map<String, String> props = new TreeMap<String, String>();
                            props.put(Sasl.QOP, "auth");
                            if (mechanism.equals("GSSAPI")) {
                                props.put(Sasl.SERVER_AUTH, "TRUE");
189
                            }
190 191 192 193 194 195 196 197 198 199
                            SaslServer ss = Sasl.createSaslServer(mechanism, "xmpp",
                                    session.getServerName(), props,
                                    new XMPPCallbackHandler());
                            // evaluateResponse doesn't like null parameter
                            byte[] token = new byte[0];
                            if (doc.isTextOnly()) {
                                // If auth request includes a value then validate it
                                token = StringUtils.decodeBase64(doc.getTextTrim());
                                if (token == null) {
                                    token = new byte[0];
200
                                }
201
                            }
202 203 204 205 206 207 208 209 210 211 212
                            byte[] challenge = ss.evaluateResponse(token);
                            // Send the challenge
                            sendChallenge(session, challenge);

                            session.setSessionData("SaslServer", ss);
                            status = Status.needResponse;
                        }
                        catch (SaslException e) {
                            Log.warn("SaslException", e);
                            authenticationFailed(session);
                            status = Status.failed;
213
                        }
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
                    }
                    else {
                        Log.warn("Client wants to do a MECH we don't support: '" +
                                mechanism + "'");
                        authenticationFailed(session);
                        status = Status.failed;
                    }
                    break;
                case RESPONSE:
                    // Store the requested SASL mechanism by the client
                    mechanism = (String) session.getSessionData("SaslMechanism");
                    if (mechanism.equalsIgnoreCase("PLAIN") &&
                            getSupportedMechanisms().contains("PLAIN")) {
                        status = doPlainAuthentication(session, doc);
                    }
                    else if (mechanism.equalsIgnoreCase("EXTERNAL")) {
                        status = doExternalAuthentication(session, doc);
                    }
                    else if (getSupportedMechanisms().contains(mechanism)) {
233 234
                        SaslServer ss = (SaslServer) session.getSessionData("SaslServer");
                        if (ss != null) {
235
                            boolean ssComplete = ss.isComplete();
236 237
                            String response = doc.getTextTrim();
                            try {
238
                                if (ssComplete) {
239 240 241
                                    authenticationSuccessful(session, ss.getAuthorizationID(),
                                            null);
                                    status = Status.authenticated;
242 243
                                }
                                else {
244 245 246 247
                                    byte[] data = StringUtils.decodeBase64(response);
                                    if (data == null) {
                                        data = new byte[0];
                                    }
248
                                    byte[] challenge = ss.evaluateResponse(data);
249
                                    if (ss.isComplete()) {
250
                                        authenticationSuccessful(session, ss.getAuthorizationID(),
251
                                                challenge);
252
                                        status = Status.authenticated;
253 254 255
                                    }
                                    else {
                                        // Send the challenge
256 257
                                        sendChallenge(session, challenge);
                                        status = Status.needResponse;
258
                                    }
259 260 261 262
                                }
                            }
                            catch (SaslException e) {
                                Log.warn("SaslException", e);
263 264
                                authenticationFailed(session);
                                status = Status.failed;
265 266 267 268
                            }
                        }
                        else {
                            Log.fatal("SaslServer is null, should be valid object instead.");
269 270
                            authenticationFailed(session);
                            status = Status.failed;
271
                        }
272
                    }
273 274 275 276 277 278 279 280 281 282 283 284
                    else {
                        Log.warn(
                                "Client responded to a MECH we don't support: '" + mechanism + "'");
                        authenticationFailed(session);
                        status = Status.failed;
                    }
                    break;
                default:
                    authenticationFailed(session);
                    status = Status.failed;
                    // Ignore
                    break;
285
            }
286
        }
287 288 289 290 291 292 293 294 295 296 297 298 299
        else {
            Log.debug("Unknown namespace sent in auth element: " + doc.asXML());
            authenticationFailed(session);
            status = Status.failed;
        }
        // Check if SASL authentication has finished so we can clean up temp information
        if (status == Status.failed || status == Status.authenticated) {
            // Remove the SaslServer from the Session
            session.removeSessionData("SaslServer");
            // Remove the requested SASL mechanism by the client
            session.removeSessionData("SaslMechanism");
        }
        return status;
300 301
    }

302
    private static Status doAnonymousAuthentication(Session session) {
303
        if (XMPPServer.getInstance().getIQAuthHandler().isAnonymousAllowed()) {
304
            // Just accept the authentication :)
305 306
            authenticationSuccessful(session, null, null);
            return Status.authenticated;
307 308 309
        }
        else {
            // anonymous login is disabled so close the connection
310 311
            authenticationFailed(session);
            return Status.failed;
312 313 314
        }
    }

315 316 317 318
    private static Status doPlainAuthentication(Session session, Element doc)
            throws UnsupportedEncodingException {
        String username;
        String password;
319
        String response = doc.getTextTrim();
320 321
        if (response == null || response.length() == 0) {
            // No info was provided so send a challenge to get it
322 323
            sendChallenge(session, new byte[0]);
            return Status.needResponse;
324 325
        }

326 327 328 329 330 331
        // Parse data and obtain username & password
        String data = new String(StringUtils.decodeBase64(response), CHARSET);
        StringTokenizer tokens = new StringTokenizer(data, "\0");
        if (tokens.countTokens() > 2) {
            // Skip the "authorization identity"
            tokens.nextToken();
332
        }
333 334
        username = tokens.nextToken();
        password = tokens.nextToken();
335 336
        try {
            AuthToken token = AuthFactory.authenticate(username, password);
337 338
            authenticationSuccessful(session, token.getUsername(), null);
            return Status.authenticated;
339 340
        }
        catch (UnauthorizedException e) {
341 342
            authenticationFailed(session);
            return Status.failed;
343 344 345
        }
    }

346 347
    private static Status doExternalAuthentication(Session session, Element doc)
            throws UnsupportedEncodingException {
348 349
        // Only accept EXTERNAL SASL for s2s. At this point the connection has already
        // been secured using TLS
350
        if (!(session instanceof IncomingServerSession)) {
351
            return Status.failed;
352 353
        }
        String hostname = doc.getTextTrim();
354
        if (hostname == null || hostname.length() == 0) {
355
            // No hostname was provided so send a challenge to get it
356 357
            sendChallenge(session, new byte[0]);
            return Status.needResponse;
358 359
        }

360 361 362 363 364 365 366 367 368 369 370 371 372
        hostname = new String(StringUtils.decodeBase64(hostname), CHARSET);
        // Check if cerificate validation is disabled for s2s
        // Flag that indicates if certificates of the remote server should be validated.
        // Disabling certificate validation is not recommended for production environments.
        boolean verify =
                JiveGlobals.getBooleanProperty("xmpp.server.certificate.verify", true);
        if (!verify) {
            authenticationSuccessful(session, hostname, null);
            return Status.authenticated;
        }
        // Check that hostname matches the one provided in a certificate
        SocketConnection connection = (SocketConnection) session.getConnection();
        try {
373
            for (Certificate certificate : connection.getSSLSession().getPeerCertificates()) {
374 375
                if (TLSStreamHandler.getPeerIdentities((X509Certificate) certificate)
                        .contains(hostname)) {
376 377
                    authenticationSuccessful(session, hostname, null);
                    return Status.authenticated;
378
                }
379 380
            }
        }
381 382 383 384 385
        catch (SSLPeerUnverifiedException e) {
            Log.warn("Error retrieving client certificates of: " + session, e);
        }
        authenticationFailed(session);
        return Status.failed;
386 387
    }

388
    private static void sendChallenge(Session session, byte[] challenge) {
389
        StringBuilder reply = new StringBuilder(250);
390 391 392
        if(challenge == null) {
            challenge = new byte[0];
        }
393 394 395 396
        String challenge_b64 = StringUtils.encodeBase64(challenge).trim();
        if ("".equals(challenge_b64)) {
            challenge_b64 = "="; // Must be padded if null
        }
397 398
        reply.append(
                "<challenge xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
399
        reply.append(challenge_b64);
400
        reply.append("</challenge>");
401
        session.getConnection().deliverRawText(reply.toString());
402 403
    }

404 405
    private static void authenticationSuccessful(Session session, String username,
            byte[] successData) {
406 407 408 409 410 411 412 413
        StringBuilder reply = new StringBuilder(80);
        reply.append("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"");
        if (successData != null) {
            reply.append(">").append(successData).append("</success>");
        }
        else {
            reply.append("/>");
        }
414
        session.getConnection().deliverRawText(reply.toString());
415 416 417 418 419 420 421 422 423 424 425 426 427 428
        // We only support SASL for c2s
        if (session instanceof ClientSession) {
            ((ClientSession) session).setAuthToken(new AuthToken(username));
        }
        else if (session instanceof IncomingServerSession) {
            String hostname = username;
            // Set the first validated domain as the address of the session
            session.setAddress(new JID(null, hostname, null));
            // Add the validated domain as a valid domain. The remote server can
            // now send packets from this address
            ((IncomingServerSession) session).addValidatedDomain(hostname);
        }
    }

429
    private static void authenticationFailed(Session session) {
430 431 432
        StringBuilder reply = new StringBuilder(80);
        reply.append("<failure xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
        reply.append("<not-authorized/></failure>");
433
        session.getConnection().deliverRawText(reply.toString());
434 435 436
        // Give a number of retries before closing the connection
        Integer retries = (Integer) session.getSessionData("authRetries");
        if (retries == null) {
437
            retries = 1;
438 439 440 441 442 443 444
        }
        else {
            retries = retries + 1;
        }
        session.setSessionData("authRetries", retries);
        if (retries >= JiveGlobals.getIntProperty("xmpp.auth.retries", 3) ) {
            // Close the connection
445
            session.getConnection().close();
446
        }
447
    }
448 449

    /**
450 451
     * Adds a new SASL mechanism to the list of supported SASL mechanisms by the server. The
     * new mechanism will be offered to clients and connection managers as stream features.
452 453 454
     *
     * @param mechanism the new SASL mechanism.
     */
455
    public static void addSupportedMechanism(String mechanism) {
456 457 458 459 460 461 462 463
        mechanisms.add(mechanism);
    }

    /**
     * Removes a SASL mechanism from the list of supported SASL mechanisms by the server.
     *
     * @param mechanism the SASL mechanism to remove.
     */
464
    public static void removeSupportedMechanism(String mechanism) {
465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488
        mechanisms.remove(mechanism);
    }

    /**
     * Returns the list of supported SASL mechanisms by the server. Note that Java may have
     * support for more mechanisms but some of them may not be returned since a special setup
     * is required that might be missing. Use {@link #addSupportedMechanism(String)} to add
     * new SASL mechanisms.
     *
     * @return the list of supported SASL mechanisms by the server.
     */
    public static Set<String> getSupportedMechanisms() {
        return mechanisms;
    }

    private static void initMechanisms() {
        mechanisms = new HashSet<String>();
        String available = JiveGlobals.getXMLProperty("sasl.mechs");
        if (available == null) {
            mechanisms.add("ANONYMOUS");
            mechanisms.add("PLAIN");
            mechanisms.add("DIGEST-MD5");
            mechanisms.add("CRAM-MD5");
        } else {
489 490 491 492
            StringTokenizer st = new StringTokenizer(available, " ,\t\n\r\f");
            while (st.hasMoreTokens()) {
                String mech = st.nextToken().toUpperCase();
                // Check that the mech is a supported mechansim. Maybe we shouldnt check this and allow any?
493 494 495 496 497 498
                if (mech.equals("ANONYMOUS") ||
                        mech.equals("PLAIN") ||
                        mech.equals("DIGEST-MD5") ||
                        mech.equals("CRAM-MD5") ||
                        mech.equals("GSSAPI")) {
                    Log.debug("SASLAuthentication: Added " + mech + " to mech list");
499 500 501
                    mechanisms.add(mech);
                }
            }
502 503 504 505 506 507 508 509 510

            if (getSupportedMechanisms().contains("GSSAPI")) {
                if (JiveGlobals.getXMLProperty("sasl.gssapi.config") != null) {
                    System.setProperty("java.security.krb5.debug",
                            JiveGlobals.getXMLProperty("sasl.gssapi.debug", "false"));
                    System.setProperty("java.security.auth.login.config",
                            JiveGlobals.getXMLProperty("sasl.gssapi.config"));
                    System.setProperty("javax.security.auth.useSubjectCredsOnly",
                            JiveGlobals.getXMLProperty("sasl.gssapi.useSubjectCredsOnly", "false"));
511 512 513 514 515 516 517 518
                } else {
                    //Not configured, remove the option.
                    Log.debug("SASLAuthentication: Removed GSSAPI from mech list");
                    mechanisms.remove("GSSAPI");
                }
            }
        }
    }
519
}