1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
58
59
60
61
62
63
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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
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
138
139
140
141
142
143
144
145
146
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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
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
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
/**
* $RCSfile$
* $Revision: 2814 $
* $Date: 2005-09-13 16:41:10 -0300 (Tue, 13 Sep 2005) $
*
* Copyright (C) 2004-2006 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.openfire.auth;
import org.jivesoftware.util.*;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.openfire.lockout.LockOutManager;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
/**
* Pluggable authentication service. Users of Openfire that wish to change the AuthProvider
* implementation used to authenticate users can set the <code>AuthProvider.className</code>
* XML property. For example, if you have configured Openfire to use LDAP for user information,
* you'd want to send a custom implementation of AuthFactory to make LDAP auth queries.
* After changing the <code>AuthProvider.className</code> XML property, you must restart your
* application server.
*
* @author Matt Tucker
*/
public class AuthFactory {
private static AuthProvider authProvider = null;
private static MessageDigest digest;
private static final Object DIGEST_LOCK = new Object();
private static Blowfish cipher = null;
static {
// Create a message digest instance.
try {
digest = MessageDigest.getInstance("SHA");
}
catch (NoSuchAlgorithmException e) {
Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
}
// 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) {
//Ignore
}
public void propertyDeleted(String property, Map params) {
//Ignore
}
public void xmlPropertySet(String property, Map params) {
if ("provider.auth.className".equals(property)) {
initProvider();
}
}
public void xmlPropertyDeleted(String property, Map params) {
//Ignore
}
};
PropertyEventDispatcher.addListener(propListener);
}
private static void initProvider() {
String className = JiveGlobals.getXMLProperty("provider.auth.className",
"org.jivesoftware.openfire.auth.DefaultAuthProvider");
// 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();
}
}
}
/**
* 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;
}
/**
* 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 {
return authProvider.getPassword(username.toLowerCase());
}
/**
* 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.
* @throws UnauthorizedException if the username and password do not match any existing user
* or the account is locked out.
*/
public static AuthToken authenticate(String username, String password)
throws UnauthorizedException
{
if (LockOutManager.getInstance().isAccountDisabled(username)) {
LockOutManager.getInstance().recordFailedLogin(username);
throw new UnauthorizedException();
}
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
* existing user or the account is locked out.
*/
public static AuthToken authenticate(String username, String token, String digest)
throws UnauthorizedException
{
if (LockOutManager.getInstance().isAccountDisabled(username)) {
LockOutManager.getInstance().recordFailedLogin(username);
throw new UnauthorizedException();
}
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) {
synchronized (DIGEST_LOCK) {
digest.update(token.getBytes());
return StringUtils.encodeHex(digest.digest(password.getBytes()));
}
}
/**
* 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) {
if (password == null) {
return null;
}
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) {
if (encryptedPassword == null) {
return null;
}
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.
*
* @return the Blowfish cipher, or <tt>null</tt> if Openfire is not able to create a Cipher;
* 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;
}
}