JDBCAuthProvider.java 14.4 KB
Newer Older
1 2 3 4
/**
 * $Revision: 1116 $
 * $Date: 2005-03-10 20:18:08 -0300 (Thu, 10 Mar 2005) $
 *
5
 * Copyright (C) 2005-2008 Jive Software. All rights reserved.
6
 *
7 8 9 10 11 12 13 14 15 16 17
 * 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.
18 19
 */

20
package org.jivesoftware.openfire.auth;
21

22 23 24 25 26 27
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.user.UserAlreadyExistsException;
import org.jivesoftware.openfire.user.UserManager;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.JiveGlobals;
28 29 30
import org.jivesoftware.util.Log;
import org.jivesoftware.util.StringUtils;

31
import java.sql.*;
32 33 34 35 36 37

/**
 * The JDBC auth provider allows you to authenticate users against any database
 * that you can connect to with JDBC. It can be used along with the
 * {@link HybridAuthProvider hybrid} auth provider, so that you can also have
 * XMPP-only users that won't pollute your external data.<p>
38
 *
39 40 41 42
 * To enable this provider, set the following in the system properties:
 * <ul>
 * <li><tt>provider.auth.className = org.jivesoftware.openfire.auth.JDBCAuthProvider</tt></li>
 * </ul>
43
 *
44
 * You'll also need to set your JDBC driver, connection string, and SQL statements:
45
 *
46 47 48 49 50
 * <ul>
 * <li><tt>jdbcProvider.driver = com.mysql.jdbc.Driver</tt></li>
 * <li><tt>jdbcProvider.connectionString = jdbc:mysql://localhost/dbname?user=username&amp;password=secret</tt></li>
 * <li><tt>jdbcAuthProvider.passwordSQL = SELECT password FROM user_account WHERE username=?</tt></li>
 * <li><tt>jdbcAuthProvider.passwordType = plain</tt></li>
51 52
 * <li><tt>jdbcAuthProvider.allowUpdate = true</tt></li>
 * <li><tt>jdbcAuthProvider.setPasswordSQL = UPDATE user_account SET password=? WHERE username=?</tt></li>
53
 * </ul>
54
 *
55 56 57 58 59 60 61
 * In order to use the configured JDBC connection provider do not use a JDBC
 * connection string, set the following property
 *
 * <ul>
 * <li><tt>jdbcAuthProvider.useConnectionProvider = true</tt></li>
 * </ul>
 *
62
 * The passwordType setting tells Openfire how the password is stored. Setting the value
63 64 65 66 67
 * is optional (when not set, it defaults to "plain"). The valid values are:<ul>
 *      <li>{@link PasswordType#plain plain}
 *      <li>{@link PasswordType#md5 md5}
 *      <li>{@link PasswordType#sha1 sha1}
 *  </ul>
68 69 70 71 72
 *
 * @author David Snopek
 */
public class JDBCAuthProvider implements AuthProvider {

73 74
    private String connectionString;

75
    private String passwordSQL;
76
    private String setPasswordSQL;
77
    private PasswordType passwordType;
78
    private boolean allowUpdate;
79
    private boolean useConnectionProvider;
80 81 82 83 84

    /**
     * Constructs a new JDBC authentication provider.
     */
    public JDBCAuthProvider() {
85 86 87 88 89
        // Convert XML based provider setup to Database based
        JiveGlobals.migrateProperty("jdbcProvider.driver");
        JiveGlobals.migrateProperty("jdbcProvider.connectionString");
        JiveGlobals.migrateProperty("jdbcAuthProvider.passwordSQL");
        JiveGlobals.migrateProperty("jdbcAuthProvider.passwordType");
90 91
        JiveGlobals.migrateProperty("jdbcAuthProvider.setPasswordSQL");
        JiveGlobals.migrateProperty("jdbcAuthProvider.allowUpdate");
92

93 94 95 96 97 98 99 100 101 102 103 104 105
        useConnectionProvider = JiveGlobals.getBooleanProperty("jdbcAuthProvider.useConnectionProvider");

        if (!useConnectionProvider) {
            // Load the JDBC driver and connection string.
            String jdbcDriver = JiveGlobals.getProperty("jdbcProvider.driver");
            try {
               Class.forName(jdbcDriver).newInstance();
            }
            catch (Exception e) {
                Log.error("Unable to load JDBC driver: " + jdbcDriver, e);
                return;
            }
            connectionString = JiveGlobals.getProperty("jdbcProvider.connectionString");
106
        }
107

108
        // Load SQL statements.
109
        passwordSQL = JiveGlobals.getProperty("jdbcAuthProvider.passwordSQL");
110 111 112 113
        setPasswordSQL = JiveGlobals.getProperty("jdbcAuthProvider.setPasswordSQL");

        allowUpdate = JiveGlobals.getBooleanProperty("jdbcAuthProvider.allowUpdate",false);

114 115
        passwordType = PasswordType.plain;
        try {
116
            passwordType = PasswordType.valueOf(
117
                    JiveGlobals.getProperty("jdbcAuthProvider.passwordType", "plain"));
118 119 120 121
        }
        catch (IllegalArgumentException iae) {
            Log.error(iae);
        }
122 123 124 125 126 127 128
    }

    public void authenticate(String username, String password) throws UnauthorizedException {
        if (username == null || password == null) {
            throw new UnauthorizedException();
        }
        username = username.trim().toLowerCase();
129 130 131 132
        if (username.contains("@")) {
            // Check that the specified domain matches the server's domain
            int index = username.indexOf("@");
            String domain = username.substring(index + 1);
133
            if (domain.equals(XMPPServer.getInstance().getServerInfo().getXMPPDomain())) {
134 135 136 137 138 139
                username = username.substring(0, index);
            } else {
                // Unknown domain. Return authentication failed.
                throw new UnauthorizedException();
            }
        }
140 141
        String userPassword;
        try {
142
            userPassword = getPasswordValue(username);
143 144 145 146
        }
        catch (UserNotFoundException unfe) {
            throw new UnauthorizedException();
        }
147 148 149 150
        // If the user's password doesn't match the password passed in, authentication
        // should fail.
        if (passwordType == PasswordType.md5) {
            password = StringUtils.hash(password, "MD5");
151
        }
152 153
        else if (passwordType == PasswordType.sha1) {
            password = StringUtils.hash(password, "SHA-1");
154
        }
155 156
        if (!password.equals(userPassword)) {
            throw new UnauthorizedException();
157 158 159 160 161 162 163
        }

        // Got this far, so the user must be authorized.
        createUser(username);
    }

    public void authenticate(String username, String token, String digest)
164 165 166 167 168 169
            throws UnauthorizedException
    {
        if (passwordType != PasswordType.plain) {
            throw new UnsupportedOperationException("Digest authentication not supported for "
                    + "password type " + passwordType);
        }
170 171 172 173
        if (username == null || token == null || digest == null) {
            throw new UnauthorizedException();
        }
        username = username.trim().toLowerCase();
174 175 176 177
        if (username.contains("@")) {
            // Check that the specified domain matches the server's domain
            int index = username.indexOf("@");
            String domain = username.substring(index + 1);
178
            if (domain.equals(XMPPServer.getInstance().getServerInfo().getXMPPDomain())) {
179 180 181 182 183 184
                username = username.substring(0, index);
            } else {
                // Unknown domain. Return authentication failed.
                throw new UnauthorizedException();
            }
        }
185 186
        String password;
        try {
187
            password = getPasswordValue(username);
188 189 190 191
        }
        catch (UserNotFoundException unfe) {
            throw new UnauthorizedException();
        }
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
        String anticipatedDigest = AuthFactory.createDigest(token, password);
        if (!digest.equalsIgnoreCase(anticipatedDigest)) {
            throw new UnauthorizedException();
        }

        // Got this far, so the user must be authorized.
        createUser(username);
    }

    public boolean isPlainSupported() {
        // If the auth SQL is defined, plain text authentication is supported.
        return (passwordSQL != null);
    }

    public boolean isDigestSupported() {
        // The auth SQL must be defined and the password type is supported.
        return (passwordSQL != null && passwordType == PasswordType.plain);
    }

211 212 213
    public String getPassword(String username) throws UserNotFoundException,
            UnsupportedOperationException
    {
214

215 216 217
        if (!supportsPasswordRetrieval()) {
            throw new UnsupportedOperationException();
        }
218 219 220 221
        if (username.contains("@")) {
            // Check that the specified domain matches the server's domain
            int index = username.indexOf("@");
            String domain = username.substring(index + 1);
222
            if (domain.equals(XMPPServer.getInstance().getServerInfo().getXMPPDomain())) {
223
                username = username.substring(0, index);
224 225 226
            } else {
                // Unknown domain.
                throw new UserNotFoundException();
227 228
            }
        }
229 230 231 232 233 234
        return getPasswordValue(username);
    }

    public void setPassword(String username, String password)
            throws UserNotFoundException, UnsupportedOperationException
    {
235 236 237 238 239
        if (allowUpdate && setPasswordSQL != null) {
            setPasswordValue(username, password);
        } else { 
            throw new UnsupportedOperationException();
        }
240 241 242 243 244 245
    }

    public boolean supportsPasswordRetrieval() {
        return (passwordSQL != null && passwordType == PasswordType.plain);
    }

246 247 248 249 250 251
    private Connection getConnection() throws SQLException {
        if (useConnectionProvider)
            return DbConnectionManager.getConnection();
        return DriverManager.getConnection(connectionString);
    }

252 253 254 255
    /**
     * Returns the value of the password field. It will be in plain text or hashed
     * format, depending on the password type.
     *
256
     * @param username user to retrieve the password field for
257 258 259 260
     * @return the password value.
     * @throws UserNotFoundException if the given user could not be loaded.
     */
    private String getPasswordValue(String username) throws UserNotFoundException {
261 262
        String password = null;
        Connection con = null;
263
        PreparedStatement pstmt = null;
264
        ResultSet rs = null;
265 266 267 268
        if (username.contains("@")) {
            // Check that the specified domain matches the server's domain
            int index = username.indexOf("@");
            String domain = username.substring(index + 1);
269
            if (domain.equals(XMPPServer.getInstance().getServerInfo().getXMPPDomain())) {
270 271 272 273 274 275
                username = username.substring(0, index);
            } else {
                // Unknown domain.
                throw new UserNotFoundException();
            }
        }
276
        try {
277
            con = getConnection();
278
            pstmt = con.prepareStatement(passwordSQL);
279 280
            pstmt.setString(1, username);

281
            rs = pstmt.executeQuery();
282 283 284 285

            // If the query had no results, the username and password
            // did not match a user record. Therefore, throw an exception.
            if (!rs.next()) {
286
                throw new UserNotFoundException();
287
            }
288
            password = rs.getString(1);
289 290 291
        }
        catch (SQLException e) {
            Log.error("Exception in JDBCAuthProvider", e);
292
            throw new UserNotFoundException();
293 294
        }
        finally {
295
            DbConnectionManager.closeConnection(rs, pstmt, con);
296
        }
297
        return password;
298 299
    }

300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315
    private void setPasswordValue(String username, String password) throws UserNotFoundException {
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        if (username.contains("@")) {
            // Check that the specified domain matches the server's domain
            int index = username.indexOf("@");
            String domain = username.substring(index + 1);
            if (domain.equals(XMPPServer.getInstance().getServerInfo().getXMPPDomain())) {
                username = username.substring(0, index);
            } else {
                // Unknown domain.
                throw new UserNotFoundException();
            }
        }
        try {
316
            con = getConnection();
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
            pstmt = con.prepareStatement(setPasswordSQL);
            pstmt.setString(1, username);
            if (passwordType == PasswordType.md5) {
                password = StringUtils.hash(password, "MD5");
            }
            else if (passwordType == PasswordType.sha1) {
                password = StringUtils.hash(password, "SHA-1");
            }
            pstmt.setString(2, password);

            rs = pstmt.executeQuery();

        }
        catch (SQLException e) {
            Log.error("Exception in JDBCAuthProvider", e);
            throw new UserNotFoundException();
        }
        finally {
            DbConnectionManager.closeConnection(rs, pstmt, con);
        }
        
    }

340 341 342
    /**
     * Indicates how the password is stored.
     */
343
    @SuppressWarnings({"UnnecessarySemicolon"})  // Support for QDox Parser
344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
    public enum PasswordType {

        /**
         * The password is stored as plain text.
         */
        plain,

        /**
         * The password is stored as a hex-encoded MD5 hash.
         */
        md5,

        /**
         * The password is stored as a hex-encoded SHA-1 hash.
         */
359
        sha1;
360 361 362 363 364 365 366
    }

    /**
     * Checks to see if the user exists; if not, a new user is created.
     *
     * @param username the username.
     */
367 368 369 370 371 372 373 374
    private static void createUser(String username) {
        // See if the user exists in the database. If not, automatically create them.
        UserManager userManager = UserManager.getInstance();
        try {
            userManager.getUser(username);
        }
        catch (UserNotFoundException unfe) {
            try {
375
                Log.debug("JDBCAuthProvider: Automatically creating new user account for " + username);
376 377 378 379 380 381 382 383 384
                UserManager.getUserProvider().createUser(username, StringUtils.randomString(8),
                        null, null);
            }
            catch (UserAlreadyExistsException uaee) {
                // Ignore.
            }
        }
    }
}