IQAuthHandler.java 12.2 KB
Newer Older
1 2 3 4 5
/**
 * $RCSfile$
 * $Revision: 2747 $
 * $Date: 2005-08-31 15:12:28 -0300 (Wed, 31 Aug 2005) $
 *
6
 * Copyright (C) 2007 Jive Software. All rights reserved.
7 8 9 10 11 12 13 14 15 16
 *
 * 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.handler;

import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
17 18 19 20
import org.jivesoftware.stringprep.StringprepException;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.Log;
21 22 23 24
import org.jivesoftware.wildfire.*;
import org.jivesoftware.wildfire.auth.AuthFactory;
import org.jivesoftware.wildfire.auth.AuthToken;
import org.jivesoftware.wildfire.auth.UnauthorizedException;
25 26
import org.jivesoftware.wildfire.session.ClientSession;
import org.jivesoftware.wildfire.session.Session;
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
import org.jivesoftware.wildfire.user.UserManager;
import org.jivesoftware.wildfire.user.UserNotFoundException;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.PacketError;
import org.xmpp.packet.StreamError;

import java.util.ArrayList;
import java.util.List;

/**
 * Implements the TYPE_IQ jabber:iq:auth protocol (plain only). Clients
 * use this protocol to authenticate with the server. A 'get' query
 * runs an authentication probe with a given user name. Return the
 * authentication form or an error indicating the user is not
 * registered on the server.<p>
 *
 * A 'set' query authenticates with information given in the
 * authentication form. An authenticated session may reset their
 * authentication information using a 'set' query.
 *
 * <h2>Assumptions</h2>
 * This handler assumes that the request is addressed to the server.
 * An appropriate TYPE_IQ tag matcher should be placed in front of this
 * one to route TYPE_IQ requests not addressed to the server to
 * another channel (probably for direct delivery to the recipient).
 *
 * @author Iain Shigeoka
 */
public class IQAuthHandler extends IQHandler implements IQAuthInfo {

Matt Tucker's avatar
Matt Tucker committed
58
    private boolean anonymousAllowed;
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82

    private Element probeResponse;
    private IQHandlerInfo info;

    private UserManager userManager;
    private XMPPServer localServer;
    private SessionManager sessionManager;

    /**
     * Clients are not authenticated when accessing this handler.
     */
    public IQAuthHandler() {
        super("XMPP Authentication handler");
        info = new IQHandlerInfo("query", "jabber:iq:auth");

        probeResponse = DocumentHelper.createElement(QName.get("query", "jabber:iq:auth"));
        probeResponse.addElement("username");
        if (AuthFactory.isPlainSupported()) {
            probeResponse.addElement("password");
        }
        if (AuthFactory.isDigestSupported()) {
            probeResponse.addElement("digest");
        }
        probeResponse.addElement("resource");
Matt Tucker's avatar
Matt Tucker committed
83
        anonymousAllowed = JiveGlobals.getBooleanProperty("xmpp.auth.anonymous");
84 85 86
    }

    public IQ handleIQ(IQ packet) throws UnauthorizedException, PacketException {
87 88 89 90 91 92 93 94 95 96 97 98 99
        ClientSession session = sessionManager.getSession(packet.getFrom());
        // If no session was found then answer an error (if possible)
        if (session == null) {
            Log.error("Error during authentication. Session not found in " +
                    sessionManager.getPreAuthenticatedKeys() +
                    " for key " +
                    packet.getFrom());
            // This error packet will probably won't make it through
            IQ reply = IQ.createResultIQ(packet);
            reply.setChildElement(packet.getChildElement().createCopy());
            reply.setError(PacketError.Condition.internal_server_error);
            return reply;
        }
100
        IQ response;
101
        try {
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
            Element iq = packet.getElement();
            Element query = iq.element("query");
            Element queryResponse = probeResponse.createCopy();
            if (IQ.Type.get == packet.getType()) {
                String username = query.elementTextTrim("username");
                if (username != null) {
                    queryResponse.element("username").setText(username);
                }
                response = IQ.createResultIQ(packet);
                response.setChildElement(queryResponse);
                // This is a workaround. Since we don't want to have an incorrect TO attribute
                // value we need to clean up the TO attribute and send directly the response.
                // The TO attribute will contain an incorrect value since we are setting a fake
                // JID until the user actually authenticates with the server.
                if (session.getStatus() != Session.STATUS_AUTHENTICATED) {
                    response.setTo((JID)null);
                }
119
            }
120 121 122 123 124
            // Otherwise set query
            else {
                if (query.elements().isEmpty()) {
                    // Anonymous authentication
                    response = anonymousLogin(session, packet);
125 126
                }
                else {
127 128 129 130 131 132
                    String username = query.elementTextTrim("username");
                    // Login authentication
                    String password = query.elementTextTrim("password");
                    String digest = null;
                    if (query.element("digest") != null) {
                        digest = query.elementTextTrim("digest").toLowerCase();
133 134
                    }

135 136 137 138 139 140
                    // If we're already logged in, this is a password reset
                    if (session.getStatus() == Session.STATUS_AUTHENTICATED) {
                        response = passwordReset(password, packet, username, session);
                    }
                    else {
                        // it is an auth attempt
141
                        response = login(username, query, packet, password, session, digest);
142 143 144 145
                    }
                }
            }
        }
146 147 148 149 150 151 152 153 154
        catch (UserNotFoundException e) {
            response = IQ.createResultIQ(packet);
            response.setChildElement(packet.getChildElement().createCopy());
            response.setError(PacketError.Condition.not_authorized);
        }
        catch (UnauthorizedException e) {
            response = IQ.createResultIQ(packet);
            response.setChildElement(packet.getChildElement().createCopy());
            response.setError(PacketError.Condition.not_authorized);
155
        }
156 157 158 159
        // Send the response directly since we want to be sure that we are sending it back
        // to the correct session. Any other session of the same user but with different
        // resource is incorrect.
        session.process(response);
160 161 162
        return null;
    }

163 164 165
    private IQ login(String username, Element iq, IQ packet, String password, ClientSession session, String digest)
            throws UnauthorizedException, UserNotFoundException {
        // Verify that specified resource is not violating any string prep rule
166 167 168
        String resource = iq.elementTextTrim("resource");
        if (resource != null) {
            try {
169
                resource = JID.resourceprep(resource);
170 171 172 173 174 175 176
            }
            catch (StringprepException e) {
                throw new IllegalArgumentException("Invalid resource: " + resource);
            }
        }
        else {
            // Answer a not_acceptable error since a resource was not supplied
177
            IQ response = IQ.createResultIQ(packet);
178 179
            response.setChildElement(packet.getChildElement().createCopy());
            response.setError(PacketError.Condition.not_acceptable);
180
            return response;
181
        }
182
        username = username.toLowerCase();
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
        // Verify that supplied username and password are correct (i.e. user authentication was successful)
        AuthToken token = null;
        if (password != null && AuthFactory.isPlainSupported()) {
            token = AuthFactory.authenticate(username, password);
        }
        else if (digest != null && AuthFactory.isDigestSupported()) {
            token = AuthFactory.authenticate(username, session.getStreamID().toString(),
                    digest);
        }
        if (token == null) {
            throw new UnauthorizedException();
        }
        // Verify if there is a resource conflict between new resource and existing one.
        // Check if a session already exists with the requested full JID and verify if
        // we should kick it off or refuse the new connection
198
        if (sessionManager.isActiveRoute(username, resource)) {
199
            ClientSession oldSession;
200 201 202 203 204 205 206 207
            try {
                String domain = localServer.getServerInfo().getName();
                oldSession = sessionManager.getSession(username, domain, resource);
                oldSession.incrementConflictCount();
                int conflictLimit = sessionManager.getConflictKickLimit();
                if (conflictLimit != SessionManager.NEVER_KICK && oldSession.getConflictCount() > conflictLimit) {
                    Connection conn = oldSession.getConnection();
                    if (conn != null) {
208
                        // Send a stream:error before closing the old connection
209
                        StreamError error = new StreamError(StreamError.Condition.conflict);
210
                        conn.deliverRawText(error.toXML());
211 212 213 214
                        conn.close();
                    }
                }
                else {
215
                    IQ response = IQ.createResultIQ(packet);
216 217
                    response.setChildElement(packet.getChildElement().createCopy());
                    response.setError(PacketError.Condition.forbidden);
218
                    return response;
219 220 221 222 223 224
                }
            }
            catch (Exception e) {
                Log.error("Error during login", e);
            }
        }
225
        // Set that the new session has been authenticated successfully
226
        session.setAuthToken(token, resource);
227 228
        packet.setFrom(session.getAddress());
        return IQ.createResultIQ(packet);
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
    }

    private IQ passwordReset(String password, IQ packet, String username, Session session)
            throws UnauthorizedException
    {
        IQ response;
        if (password == null || password.length() == 0) {
            throw new UnauthorizedException();
        }
        else {
            try {
                userManager.getUser(username).setPassword(password);
                response = IQ.createResultIQ(packet);
                List<String> params = new ArrayList<String>();
                params.add(username);
                params.add(session.toString());
                Log.info(LocaleUtils.getLocalizedString("admin.password.update", params));
            }
            catch (UserNotFoundException e) {
                throw new UnauthorizedException();
            }
        }
        return response;
    }

    private IQ anonymousLogin(ClientSession session, IQ packet) {
255
        IQ response = IQ.createResultIQ(packet);
256 257
        if (anonymousAllowed) {
            session.setAnonymousAuth();
258
            response.setTo(session.getAddress());
259 260 261 262 263 264 265 266 267 268
            Element auth = response.setChildElement("query", "jabber:iq:auth");
            auth.addElement("resource").setText(session.getAddress().getResource());
        }
        else {
            response.setChildElement(packet.getChildElement().createCopy());
            response.setError(PacketError.Condition.forbidden);
        }
        return response;
    }

Gaston Dombiak's avatar
Gaston Dombiak committed
269
    public boolean isAnonymousAllowed() {
270 271 272 273 274
        return anonymousAllowed;
    }

    public void setAllowAnonymous(boolean isAnonymous) throws UnauthorizedException {
        anonymousAllowed = isAnonymous;
275
        JiveGlobals.setProperty("xmpp.auth.anonymous", Boolean.toString(anonymousAllowed));
276 277 278 279 280 281 282 283 284 285 286 287 288
    }

    public void initialize(XMPPServer server) {
        super.initialize(server);
        localServer = server;
        userManager = server.getUserManager();
        sessionManager = server.getSessionManager();
    }

    public IQHandlerInfo getInfo() {
        return info;
    }
}