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