AuthFactory.java 11 KB
Newer Older
1 2 3 4 5
/**
 * $RCSfile$
 * $Revision: 2814 $
 * $Date: 2005-09-13 16:41:10 -0300 (Tue, 13 Sep 2005) $
 *
6
 * Copyright (C) 2004-2008 Jive Software. All rights reserved.
7 8
 *
 * This software is published under the terms of the GNU Public License (GPL),
9 10
 * a copy of which is included in this distribution, or a commercial license
 * agreement with Jive.
11 12
 */

13
package org.jivesoftware.openfire.auth;
14 15

import org.jivesoftware.util.*;
16
import org.jivesoftware.openfire.user.UserNotFoundException;
17
import org.jivesoftware.openfire.lockout.LockOutManager;
18 19 20

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
21
import java.util.Map;
22 23

/**
24
 * Pluggable authentication service. Users of Openfire that wish to change the AuthProvider
Matt Tucker's avatar
Matt Tucker committed
25
 * implementation used to authenticate users can set the <code>AuthProvider.className</code>
26
 * system property. For example, if you have configured Openfire to use LDAP for user information,
Matt Tucker's avatar
Matt Tucker committed
27
 * you'd want to send a custom implementation of AuthFactory to make LDAP auth queries.
28
 * After changing the <code>AuthProvider.className</code> system property, you must restart your
Matt Tucker's avatar
Matt Tucker committed
29
 * application server.
30 31 32 33 34 35 36
 *
 * @author Matt Tucker
 */
public class AuthFactory {

    private static AuthProvider authProvider = null;
    private static MessageDigest digest;
37 38
    private static final Object DIGEST_LOCK = new Object();
    private static Blowfish cipher = null;
39 40 41 42 43 44 45 46 47

    static {
        // Create a message digest instance.
        try {
            digest = MessageDigest.getInstance("SHA");
        }
        catch (NoSuchAlgorithmException e) {
            Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
        }
48 49 50 51 52 53
        // Load an auth provider.
        initProvider();

        // Detect when a new auth provider class is set 
        PropertyEventListener propListener = new PropertyEventListener() {
            public void propertySet(String property, Map params) {
54 55 56
                if ("provider.auth.className".equals(property)) {
                    initProvider();
                }
57 58 59 60 61 62 63
            }

            public void propertyDeleted(String property, Map params) {
                //Ignore
            }

            public void xmlPropertySet(String property, Map params) {
64
                //Ignore
65 66 67 68 69 70 71 72 73 74
            }

            public void xmlPropertyDeleted(String property, Map params) {
                //Ignore
            }
        };
        PropertyEventDispatcher.addListener(propListener);
    }

    private static void initProvider() {
75 76 77 78
        // Convert XML based provider setup to Database based
        JiveGlobals.migrateProperty("provider.auth.className");

        String className = JiveGlobals.getProperty("provider.auth.className",
79
                "org.jivesoftware.openfire.auth.DefaultAuthProvider");
80 81 82 83 84 85 86 87 88 89 90
        // Check if we need to reset the auth provider class 
        if (authProvider == null || !className.equals(authProvider.getClass().getName())) {
            try {
                Class c = ClassUtils.forName(className);
                authProvider = (AuthProvider)c.newInstance();
            }
            catch (Exception e) {
                Log.error("Error loading auth provider: " + className, e);
                authProvider = new DefaultAuthProvider();
            }
        }
91 92
    }

93 94 95 96 97 98 99 100 101 102 103 104
    /**
     * Returns the currently-installed AuthProvider. <b>Warning:</b> in virtually all
     * cases the auth provider should not be used directly. Instead, the appropriate
     * methods in AuthFactory should be called. Direct access to the auth provider is
     * only provided for special-case logic.
     *
     * @return the current UserProvider.
     */
    public static AuthProvider getAuthProvider() {
        return authProvider;
    }

105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
    /**
     * Returns true if the currently installed {@link AuthProvider} supports authentication
     * using plain-text passwords according to JEP-0078. Plain-text authentication is
     * not secure and should generally only be used over a TLS/SSL connection.
     *
     * @return true if plain text password authentication is supported.
     */
    public static boolean isPlainSupported() {
        return authProvider.isPlainSupported();
    }

    /**
     * Returns true if the currently installed {@link AuthProvider} supports
     * digest authentication according to JEP-0078.
     *
     * @return true if digest authentication is supported.
     */
    public static boolean isDigestSupported() {
        return authProvider.isDigestSupported();
    }

    /**
     * Returns the user's password. This method will throw an UnsupportedOperationException
     * if this operation is not supported by the backend user store.
     *
     * @param username the username of the user.
     * @return the user's password.
     * @throws UserNotFoundException if the given user could not be found.
     * @throws UnsupportedOperationException if the provider does not
     *      support the operation (this is an optional operation).
     */
    public static String getPassword(String username) throws UserNotFoundException,
            UnsupportedOperationException {
138
        return authProvider.getPassword(username.toLowerCase());
139 140 141 142 143 144 145 146 147 148
    }

    /**
     * Authenticates a user with a username and plain text password and returns and
     * AuthToken. If the username and password do not match the record of
     * any user in the system, this method throws an UnauthorizedException.
     *
     * @param username the username.
     * @param password the password.
     * @return an AuthToken token if the username and password are correct.
149 150
     * @throws UnauthorizedException if the username and password do not match any existing user
     *      or the account is locked out.
151 152 153 154
     */
    public static AuthToken authenticate(String username, String password)
            throws UnauthorizedException
    {
155 156 157 158
        if (LockOutManager.getInstance().isAccountDisabled(username)) {
            LockOutManager.getInstance().recordFailedLogin(username);
            throw new UnauthorizedException();
        }
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
        authProvider.authenticate(username, password);
        return new AuthToken(username);
    }

    /**
     * Authenticates a user with a username, token, and digest and returns an AuthToken.
     * The digest should be generated using the {@link #createDigest(String, String)} method.
     * If the username and digest do not match the record of any user in the system, the
     * method throws an UnauthorizedException.
     *
     * @param username the username.
     * @param token the token that was used with plain-text password to generate the digest.
     * @param digest the digest generated from plain-text password and unique token.
     * @return an AuthToken token if the username and digest are correct for the user's
     *      password and given token.
     * @throws UnauthorizedException if the username and password do not match any
175
     *      existing user or the account is locked out.
176 177 178 179
     */
    public static AuthToken authenticate(String username, String token, String digest)
            throws UnauthorizedException
    {
180 181 182 183
        if (LockOutManager.getInstance().isAccountDisabled(username)) {
            LockOutManager.getInstance().recordFailedLogin(username);
            throw new UnauthorizedException();
        }
184 185 186 187 188 189 190 191 192 193 194 195
        authProvider.authenticate(username, token, digest);
        return new AuthToken(username);
    }

    /**
     * Returns a digest given a token and password, according to JEP-0078.
     *
     * @param token the token used in the digest.
     * @param password the plain-text password to be digested.
     * @return the digested result as a hex string.
     */
    public static String createDigest(String token, String password) {
196
        synchronized (DIGEST_LOCK) {
197 198 199 200
            digest.update(token.getBytes());
            return StringUtils.encodeHex(digest.digest(password.getBytes()));
        }
    }
201 202 203 204 205 206 207 208 209 210 211 212

    /**
     * Returns an encrypted version of the plain-text password. Encryption is performed
     * using the Blowfish algorithm. The encryption key is stored as the Jive property
     * "passwordKey". If the key is not present, it will be automatically generated.
     *
     * @param password the plain-text password.
     * @return the encrypted password.
     * @throws UnsupportedOperationException if encryption/decryption is not possible;
     *      for example, during setup mode.
     */
    public static String encryptPassword(String password) {
213 214 215
        if (password == null) {
            return null;
        }
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
        Blowfish cipher = getCipher();
        if (cipher == null) {
            throw new UnsupportedOperationException();
        }
        return cipher.encryptString(password);
    }

    /**
     * Returns a decrypted version of the encrypted password. Encryption is performed
     * using the Blowfish algorithm. The encryption key is stored as the Jive property
     * "passwordKey". If the key is not present, it will be automatically generated.
     *
     * @param encryptedPassword the encrypted password.
     * @return the encrypted password.
     * @throws UnsupportedOperationException if encryption/decryption is not possible;
     *      for example, during setup mode.
     */
    public static String decryptPassword(String encryptedPassword) {
234 235 236
        if (encryptedPassword == null) {
            return null;
        }
237 238 239 240 241 242 243 244 245 246 247 248
        Blowfish cipher = getCipher();
        if (cipher == null) {
            throw new UnsupportedOperationException();
        }
        return cipher.decryptString(encryptedPassword);
    }

    /**
     * Returns a Blowfish cipher that can be used for encrypting and decrypting passwords.
     * The encryption key is stored as the Jive property "passwordKey". If it's not present,
     * it will be automatically generated.
     *
249
     * @return the Blowfish cipher, or <tt>null</tt> if Openfire is not able to create a Cipher;
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
     *      for example, during setup mode.
     */
    private static synchronized Blowfish getCipher() {
        if (cipher != null) {
            return cipher;
        }
        // Get the password key, stored as a database property. Obviously,
        // protecting your database is critical for making the
        // encryption fully secure.
        String keyString;
        try {
            keyString = JiveGlobals.getProperty("passwordKey");
            if (keyString == null) {
                keyString = StringUtils.randomString(15);
                JiveGlobals.setProperty("passwordKey", keyString);
                // Check to make sure that setting the property worked. It won't work,
                // for example, when in setup mode.
                if (!keyString.equals(JiveGlobals.getProperty("passwordKey"))) {
                    return null;
                }
            }
            cipher = new Blowfish(keyString);
        }
        catch (Exception e) {
            Log.error(e);
        }
        return cipher;
    }
278
}