1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.catalina.realm;
19
20 import java.security.Principal;
21 import java.security.cert.X509Certificate;
22 import java.util.LinkedHashMap;
23 import java.util.Map;
24 import java.util.concurrent.atomic.AtomicInteger;
25
26 import org.apache.catalina.LifecycleException;
27 import org.apache.juli.logging.Log;
28 import org.apache.juli.logging.LogFactory;
29 import org.ietf.jgss.GSSContext;
30 import org.ietf.jgss.GSSCredential;
31 import org.ietf.jgss.GSSException;
32 import org.ietf.jgss.GSSName;
33
34 /**
35 * This class extends the CombinedRealm (hence it can wrap other Realms) to
36 * provide a user lock out mechanism if there are too many failed
37 * authentication attempts in a given period of time. To ensure correct
38 * operation, there is a reasonable degree of synchronisation in this Realm.
39 * This Realm does not require modification to the underlying Realms or the
40 * associated user storage mechanisms. It achieves this by recording all failed
41 * logins, including those for users that do not exist. To prevent a DOS by
42 * deliberating making requests with invalid users (and hence causing this cache
43 * to grow) the size of the list of users that have failed authentication is
44 * limited.
45 */
46 public class LockOutRealm extends CombinedRealm {
47
48 private static final Log log = LogFactory.getLog(LockOutRealm.class);
49
50 /**
51 * The number of times in a row a user has to fail authentication to be
52 * locked out. Defaults to 5.
53 */
54 protected int failureCount = 5;
55
56 /**
57 * The time (in seconds) a user is locked out for after too many
58 * authentication failures. Defaults to 300 (5 minutes).
59 */
60 protected int lockOutTime = 300;
61
62 /**
63 * Number of users that have failed authentication to keep in cache. Over
64 * time the cache will grow to this size and may not shrink. Defaults to
65 * 1000.
66 */
67 protected int cacheSize = 1000;
68
69 /**
70 * If a failed user is removed from the cache because the cache is too big
71 * before it has been in the cache for at least this period of time (in
72 * seconds) a warning message will be logged. Defaults to 3600 (1 hour).
73 */
74 protected int cacheRemovalWarningTime = 3600;
75
76 /**
77 * Users whose last authentication attempt failed. Entries will be ordered
78 * in access order from least recent to most recent.
79 */
80 protected Map<String,LockRecord> failedUsers = null;
81
82
83 /**
84 * Prepare for the beginning of active use of the public methods of this
85 * component and implement the requirements of
86 * {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
87 *
88 * @exception LifecycleException if this component detects a fatal error
89 * that prevents this component from being used
90 */
91 @Override
92 protected synchronized void startInternal() throws LifecycleException {
93 // Configure the list of failed users to delete the oldest entry once it
94 // exceeds the specified size
95 failedUsers = new LinkedHashMap<String, LockRecord>(cacheSize, 0.75f,
96 true) {
97 private static final long serialVersionUID = 1L;
98 @Override
99 protected boolean removeEldestEntry(
100 Map.Entry<String, LockRecord> eldest) {
101 if (size() > cacheSize) {
102 // Check to see if this element has been removed too quickly
103 long timeInCache = (System.currentTimeMillis() -
104 eldest.getValue().getLastFailureTime())/1000;
105
106 if (timeInCache < cacheRemovalWarningTime) {
107 log.warn(sm.getString("lockOutRealm.removeWarning",
108 eldest.getKey(), Long.valueOf(timeInCache)));
109 }
110 return true;
111 }
112 return false;
113 }
114 };
115
116 super.startInternal();
117 }
118
119
120 /**
121 * Return the Principal associated with the specified username, which
122 * matches the digest calculated using the given parameters using the
123 * method described in RFC 2069; otherwise return <code>null</code>.
124 *
125 * @param username Username of the Principal to look up
126 * @param clientDigest Digest which has been submitted by the client
127 * @param nonce Unique (or supposedly unique) token which has been used
128 * for this request
129 * @param realmName Realm name
130 * @param md5a2 Second MD5 digest used to calculate the digest :
131 * MD5(Method + ":" + uri)
132 */
133 @Override
134 public Principal authenticate(String username, String clientDigest,
135 String nonce, String nc, String cnonce, String qop,
136 String realmName, String md5a2) {
137
138 Principal authenticatedUser = super.authenticate(username, clientDigest, nonce, nc, cnonce,
139 qop, realmName, md5a2);
140 return filterLockedAccounts(username, authenticatedUser);
141 }
142
143
144 /**
145 * Return the Principal associated with the specified username and
146 * credentials, if there is one; otherwise return <code>null</code>.
147 *
148 * @param username Username of the Principal to look up
149 * @param credentials Password or other credentials to use in
150 * authenticating this username
151 */
152 @Override
153 public Principal authenticate(String username, String credentials) {
154 Principal authenticatedUser = super.authenticate(username, credentials);
155 return filterLockedAccounts(username, authenticatedUser);
156 }
157
158
159 /**
160 * Return the Principal associated with the specified chain of X509
161 * client certificates. If there is none, return <code>null</code>.
162 *
163 * @param certs Array of client certificates, with the first one in
164 * the array being the certificate of the client itself.
165 */
166 @Override
167 public Principal authenticate(X509Certificate[] certs) {
168 String username = null;
169 if (certs != null && certs.length >0) {
170 username = certs[0].getSubjectDN().getName();
171 }
172
173 Principal authenticatedUser = super.authenticate(certs);
174 return filterLockedAccounts(username, authenticatedUser);
175 }
176
177
178 /**
179 * {@inheritDoc}
180 */
181 @Override
182 public Principal authenticate(GSSContext gssContext, boolean storeCreds) {
183 if (gssContext.isEstablished()) {
184 String username = null;
185 GSSName name = null;
186 try {
187 name = gssContext.getSrcName();
188 } catch (GSSException e) {
189 log.warn(sm.getString("realmBase.gssNameFail"), e);
190 return null;
191 }
192
193 username = name.toString();
194
195 Principal authenticatedUser = super.authenticate(gssContext, storeCreds);
196
197 return filterLockedAccounts(username, authenticatedUser);
198 }
199
200 // Fail in all other cases
201 return null;
202 }
203
204 /**
205 * {@inheritDoc}
206 */
207 @Override
208 public Principal authenticate(GSSName gssName, GSSCredential gssCredential) {
209 String username = gssName.toString();
210
211 Principal authenticatedUser = super.authenticate(gssName, gssCredential);
212
213 return filterLockedAccounts(username, authenticatedUser);
214 }
215
216
217 /*
218 * Filters authenticated principals to ensure that <code>null</code> is
219 * returned for any user that is currently locked out.
220 */
221 private Principal filterLockedAccounts(String username, Principal authenticatedUser) {
222 // Register all failed authentications
223 if (authenticatedUser == null && isAvailable()) {
224 registerAuthFailure(username);
225 }
226
227 if (isLocked(username)) {
228 // If the user is currently locked, authentication will always fail
229 log.warn(sm.getString("lockOutRealm.authLockedUser", username));
230 return null;
231 }
232
233 if (authenticatedUser != null) {
234 registerAuthSuccess(username);
235 }
236
237 return authenticatedUser;
238 }
239
240
241 /**
242 * Unlock the specified username. This will remove all records of
243 * authentication failures for this user.
244 *
245 * @param username The user to unlock
246 */
247 public void unlock(String username) {
248 // Auth success clears the lock record so...
249 registerAuthSuccess(username);
250 }
251
252 /*
253 * Checks to see if the current user is locked. If this is associated with
254 * a login attempt, then the last access time will be recorded and any
255 * attempt to authenticated a locked user will log a warning.
256 */
257 public boolean isLocked(String username) {
258 LockRecord lockRecord = null;
259 synchronized (this) {
260 lockRecord = failedUsers.get(username);
261 }
262
263 // No lock record means user can't be locked
264 if (lockRecord == null) {
265 return false;
266 }
267
268 // Check to see if user is locked
269 if (lockRecord.getFailures() >= failureCount &&
270 (System.currentTimeMillis() -
271 lockRecord.getLastFailureTime())/1000 < lockOutTime) {
272 return true;
273 }
274
275 // User has not, yet, exceeded lock thresholds
276 return false;
277 }
278
279
280 /*
281 * After successful authentication, any record of previous authentication
282 * failure is removed.
283 */
284 private synchronized void registerAuthSuccess(String username) {
285 // Successful authentication means removal from the list of failed users
286 failedUsers.remove(username);
287 }
288
289
290 /*
291 * After a failed authentication, add the record of the failed
292 * authentication.
293 */
294 private void registerAuthFailure(String username) {
295 LockRecord lockRecord = null;
296 synchronized (this) {
297 if (!failedUsers.containsKey(username)) {
298 lockRecord = new LockRecord();
299 failedUsers.put(username, lockRecord);
300 } else {
301 lockRecord = failedUsers.get(username);
302 if (lockRecord.getFailures() >= failureCount &&
303 ((System.currentTimeMillis() -
304 lockRecord.getLastFailureTime())/1000)
305 > lockOutTime) {
306 // User was previously locked out but lockout has now
307 // expired so reset failure count
308 lockRecord.setFailures(0);
309 }
310 }
311 }
312 lockRecord.registerFailure();
313 }
314
315
316 /**
317 * Get the number of failed authentication attempts required to lock the
318 * user account.
319 * @return the failureCount
320 */
321 public int getFailureCount() {
322 return failureCount;
323 }
324
325
326 /**
327 * Set the number of failed authentication attempts required to lock the
328 * user account.
329 * @param failureCount the failureCount to set
330 */
331 public void setFailureCount(int failureCount) {
332 this.failureCount = failureCount;
333 }
334
335
336 /**
337 * Get the period for which an account will be locked.
338 * @return the lockOutTime
339 */
340 public int getLockOutTime() {
341 return lockOutTime;
342 }
343
344
345 /**
346 * Set the period for which an account will be locked.
347 * @param lockOutTime the lockOutTime to set
348 */
349 public void setLockOutTime(int lockOutTime) {
350 this.lockOutTime = lockOutTime;
351 }
352
353
354 /**
355 * Get the maximum number of users for which authentication failure will be
356 * kept in the cache.
357 * @return the cacheSize
358 */
359 public int getCacheSize() {
360 return cacheSize;
361 }
362
363
364 /**
365 * Set the maximum number of users for which authentication failure will be
366 * kept in the cache.
367 * @param cacheSize the cacheSize to set
368 */
369 public void setCacheSize(int cacheSize) {
370 this.cacheSize = cacheSize;
371 }
372
373
374 /**
375 * Get the minimum period a failed authentication must remain in the cache
376 * to avoid generating a warning if it is removed from the cache to make
377 * space for a new entry.
378 * @return the cacheRemovalWarningTime
379 */
380 public int getCacheRemovalWarningTime() {
381 return cacheRemovalWarningTime;
382 }
383
384
385 /**
386 * Set the minimum period a failed authentication must remain in the cache
387 * to avoid generating a warning if it is removed from the cache to make
388 * space for a new entry.
389 * @param cacheRemovalWarningTime the cacheRemovalWarningTime to set
390 */
391 public void setCacheRemovalWarningTime(int cacheRemovalWarningTime) {
392 this.cacheRemovalWarningTime = cacheRemovalWarningTime;
393 }
394
395
396 protected static class LockRecord {
397 private final AtomicInteger failures = new AtomicInteger(0);
398 private long lastFailureTime = 0;
399
400 public int getFailures() {
401 return failures.get();
402 }
403
404 public void setFailures(int theFailures) {
405 failures.set(theFailures);
406 }
407
408 public long getLastFailureTime() {
409 return lastFailureTime;
410 }
411
412 public void registerFailure() {
413 failures.incrementAndGet();
414 lastFailureTime = System.currentTimeMillis();
415 }
416 }
417 }
418