From: markt Date: Tue, 23 Sep 2008 16:49:25 +0000 (+0000) Subject: Add new LockOut Realm plus docs X-Git-Url: https://git.internetallee.de/?a=commitdiff_plain;h=fff82e2726bdac49543cdacfcb603f446d6bd62e;p=tomcat7.0 Add new LockOut Realm plus docs git-svn-id: https://svn.apache.org/repos/asf/tomcat/trunk@698236 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/java/org/apache/catalina/realm/LocalStrings.properties b/java/org/apache/catalina/realm/LocalStrings.properties index 1bb9d2a8c..03f5ecb57 100644 --- a/java/org/apache/catalina/realm/LocalStrings.properties +++ b/java/org/apache/catalina/realm/LocalStrings.properties @@ -92,4 +92,6 @@ combinedRealm.getPrincipal=The getPrincipal() method should never be called combinedRealm.authStart=Attempting to authenticate user "{0}" with realm "{1}" combinedRealm.authFailed=Failed to authenticate user "{0}" with realm "{1}" combinedRealm.authSucess=Authenticated user "{0}" with realm "{1}" -combinedRealm.addRealm=Add "{0}" realm, making a total of "{1}" realms \ No newline at end of file +combinedRealm.addRealm=Add "{0}" realm, making a total of "{1}" realms +lockOutRealm.authLockedUser=An attempt was made to authenticate the locked user "{0}" +lockOutRealm.removeWarning=User "{0}" was removed from the failed users cache after {1} seconds to keep the cache size within the limit set \ No newline at end of file diff --git a/java/org/apache/catalina/realm/LockOutRealm.java b/java/org/apache/catalina/realm/LockOutRealm.java new file mode 100644 index 000000000..8ee69d2dc --- /dev/null +++ b/java/org/apache/catalina/realm/LockOutRealm.java @@ -0,0 +1,415 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.catalina.realm; + +import java.security.Principal; +import java.security.cert.X509Certificate; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.catalina.LifecycleException; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * This class extends the CombinedRealm (hence it can wrap other Realms) to + * provide a user lock out mechanism if there are too many failed + * authentication attempts in a given period of time. To ensure correct + * operation, there is a reasonable degree of synchronisation in this Realm. + * This Realm does not require modification to the underlying Realms or the + * associated user storage mecahisms. It achieves this by recording all failed + * logins, including those for users that do not exist. To prevent a DOS by + * deliberating making requests with invalid users (and hence causing this cache + * to grow) the size of the list of users that have failed authentication is + * limited. + */ +public class LockOutRealm extends CombinedRealm { + + private static Log log = LogFactory.getLog(LockOutRealm.class); + + /** + * The number of times in a row a user has to fail authentication to be + * locked out. Defaults to 5. + */ + protected int failureCount = 5; + + /** + * The time (in seconds) a user is locked out for after too many + * authentication failures. Defaults to 300 (5 minutes). + */ + protected int lockOutTime = 300; + + /** + * Number of users that have failed authentication to keep in cache. Over + * time the cache will grow to this size and may not shrink. Defaults to + * 1000. + */ + protected int cacheSize = 1000; + + /** + * If a failed user is removed from the cache because the cache is too big + * before it has been in the cache for at least this period of time (in + * seconds) a warning message will be logged. Defaults to 3600 (1 hour). + */ + protected int cacheRemovalWarningTime = 3600; + + /** + * Users whose last authentication attempt failed. Entries will be ordered + * in access order from least recent to most recent. + */ + protected Map failedUsers = null; + + + /** + * Prepare for the beginning of active use of the public methods of this + * component. This method should be called before any of the public + * methods of this component are utilized. It should also send a + * LifecycleEvent of type START_EVENT to any registered listeners. + * + * @exception LifecycleException if this component detects a fatal error + * that prevents this component from being used + */ + public void start() throws LifecycleException { + // Configure the list of failed users to delete the oldest entry once it + // exceeds the specified size + failedUsers = new LinkedHashMap(cacheSize, 0.75f, + true) { + protected boolean removeEldestEntry( + Map.Entry eldest) { + if (size() > cacheSize) { + // Check to see if this element has been removed too quickly + long timeInCache = (System.currentTimeMillis() - + eldest.getValue().getLastFailureTime())/1000; + + if (timeInCache < cacheRemovalWarningTime) { + log.warn(sm.getString("lockOutRealm.removeWarning", + eldest.getKey(), Long.valueOf(timeInCache))); + } + return true; + } + return false; + } + }; + + super.start(); + } + + + /** + * Return the Principal associated with the specified username and + * credentials, if there is one; otherwise return null. + * + * @param username Username of the Principal to look up + * @param credentials Password or other credentials to use in + * authenticating this username + */ + public Principal authenticate(String username, byte[] credentials) { + if (isLocked(username)) { + // Trying to authenticate a locked user is an automatic failure + registerAuthFailure(username); + + log.warn(sm.getString("lockOutRealm.authLockedUser", username)); + return null; + } + + Principal authenticatedUser = super.authenticate(username, credentials); + + if (authenticatedUser == null) { + registerAuthFailure(username); + } else { + registerAuthSuccess(username); + } + return authenticatedUser; + } + + + /** + * Return the Principal associated with the specified username, which + * matches the digest calculated using the given parameters using the + * method described in RFC 2069; otherwise return null. + * + * @param username Username of the Principal to look up + * @param clientDigest Digest which has been submitted by the client + * @param nOnce Unique (or supposedly unique) token which has been used + * for this request + * @param realm Realm name + * @param md5a2 Second MD5 digest used to calculate the digest : + * MD5(Method + ":" + uri) + */ + public Principal authenticate(String username, String clientDigest, + String once, String nc, String cnonce, String qop, + String realmName, String md5a2) { + + if (isLocked(username)) { + // Trying to authenticate a locked user is an automatic failure + registerAuthFailure(username); + + log.warn(sm.getString("lockOutRealm.authLockedUser", username)); + return null; + } + + Principal authenticatedUser = super.authenticate(username, clientDigest, + once, nc, cnonce, qop, realmName, md5a2); + + if (authenticatedUser == null) { + registerAuthFailure(username); + } else { + registerAuthSuccess(username); + } + return authenticatedUser; + } + + + /** + * Return the Principal associated with the specified username and + * credentials, if there is one; otherwise return null. + * + * @param username Username of the Principal to look up + * @param credentials Password or other credentials to use in + * authenticating this username + */ + public Principal authenticate(String username, String credentials) { + if (isLocked(username)) { + // Trying to authenticate a locked user is an automatic failure + registerAuthFailure(username); + + log.warn(sm.getString("lockOutRealm.authLockedUser", username)); + return null; + } + + Principal authenticatedUser = super.authenticate(username, credentials); + + if (authenticatedUser == null) { + registerAuthFailure(username); + } else { + registerAuthSuccess(username); + } + return authenticatedUser; + } + + + /** + * Return the Principal associated with the specified chain of X509 + * client certificates. If there is none, return null. + * + * @param certs Array of client certificates, with the first one in + * the array being the certificate of the client itself. + */ + public Principal authenticate(X509Certificate[] certs) { + String username = null; + if (certs != null && certs.length >0) { + username = certs[0].getSubjectDN().getName(); + } + + if (isLocked(username)) { + // Trying to authenticate a locked user is an automatic failure + registerAuthFailure(username); + + log.warn(sm.getString("lockOutRealm.authLockedUser", username)); + return null; + } + + Principal authenticatedUser = super.authenticate(certs); + + if (authenticatedUser == null) { + registerAuthFailure(username); + } else { + registerAuthSuccess(username); + } + return authenticatedUser; + } + + + /** + * Unlock the specified username. This will remove all records of + * authentication failures for this user. + * + * @param username The user to unlock + */ + public void unlock(String username) { + // Auth success clears the lock record so... + registerAuthSuccess(username); + } + + /* + * Checks to see if the current user is locked. If this is associated with + * a login attempt, then the last access time will be recorded and any + * attempt to authenticated a locked user will log a warning. + */ + private boolean isLocked(String username) { + LockRecord lockRecord = null; + synchronized (this) { + lockRecord = failedUsers.get(username); + } + + // No lock record means user can't be locked + if (lockRecord == null) { + return false; + } + + // Check to see if user is locked + if (lockRecord.getFailures() >= failureCount && + (System.currentTimeMillis() - + lockRecord.getLastFailureTime())/1000 < lockOutTime) { + return true; + } + + // User has not, yet, exceeded lock thresholds + return false; + } + + + /* + * After successful authentication, any record of previous authentication + * failure is removed. + */ + private synchronized void registerAuthSuccess(String username) { + // Successful authentication means removal from the list of failed users + failedUsers.remove(username); + } + + + /* + * After a failed authentication, add the record of the failed + * authentication. + */ + private void registerAuthFailure(String username) { + LockRecord lockRecord = null; + synchronized (this) { + if (!failedUsers.containsKey(username)) { + lockRecord = new LockRecord(); + failedUsers.put(username, lockRecord); + } else { + lockRecord = failedUsers.get(username); + if (lockRecord.getFailures() >= failureCount && + ((System.currentTimeMillis() - + lockRecord.getLastFailureTime())/1000) + > lockOutTime) { + // User was previously locked out but lockout has now + // expired so reset failure count + lockRecord.setFailures(0); + } + } + } + lockRecord.registerFailure(); + } + + + /** + * Get the number of failed authentication attempts required to lock the + * user account. + * @return the failureCount + */ + public int getFailureCount() { + return failureCount; + } + + + /** + * Set the number of failed authentication attempts required to lock the + * user account. + * @param failureCount the failureCount to set + */ + public void setFailureCount(int failureCount) { + this.failureCount = failureCount; + } + + + /** + * Get the period for which an account will be locked. + * @return the lockOutTime + */ + public int getLockOutTime() { + return lockOutTime; + } + + + /** + * Set the period for which an account will be locked. + * @param lockOutTime the lockOutTime to set + */ + public void setLockOutTime(int lockOutTime) { + this.lockOutTime = lockOutTime; + } + + + /** + * Get the maximum number of users for which authentication failure will be + * kept in the cache. + * @return the cacheSize + */ + public int getCacheSize() { + return cacheSize; + } + + + /** + * Set the maximum number of users for which authentication failure will be + * kept in the cache. + * @param cacheSize the cacheSize to set + */ + public void setCacheSize(int cacheSize) { + this.cacheSize = cacheSize; + } + + + /** + * Get the minimum period a failed authentication must remain in the cache + * to avoid generating a warning if it is removed from the cache to make + * space for a new entry. + * @return the cacheRemovalWarningTime + */ + public int getCacheRemovalWarningTime() { + return cacheRemovalWarningTime; + } + + + /** + * Set the minimum period a failed authentication must remain in the cache + * to avoid generating a warning if it is removed from the cache to make + * space for a new entry. + * @param cacheRemovalWarningTime the cacheRemovalWarningTime to set + */ + public void setCacheRemovalWarningTime(int cacheRemovalWarningTime) { + this.cacheRemovalWarningTime = cacheRemovalWarningTime; + } + + + protected class LockRecord { + private AtomicInteger failures = new AtomicInteger(0); + private long lastFailureTime = 0; + + public int getFailures() { + return failures.get(); + } + + public void setFailures(int theFailures) { + failures.set(theFailures); + } + + public long getLastFailureTime() { + return lastFailureTime; + } + + public void registerFailure() { + failures.incrementAndGet(); + lastFailureTime = System.currentTimeMillis(); + } + } +} diff --git a/java/org/apache/catalina/realm/mbeans-descriptors.xml b/java/org/apache/catalina/realm/mbeans-descriptors.xml index 970064277..a4c9aec35 100644 --- a/java/org/apache/catalina/realm/mbeans-descriptors.xml +++ b/java/org/apache/catalina/realm/mbeans-descriptors.xml @@ -34,8 +34,7 @@ type="java.lang.String"/> @@ -120,8 +109,7 @@ @@ -132,23 +120,19 @@ writeable="false"/> @@ -215,8 +194,7 @@ type="java.lang.String"/> @@ -270,8 +245,7 @@ @@ -282,8 +256,7 @@ writeable="false"/> @@ -294,8 +267,7 @@ @@ -311,4 +283,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/webapps/docs/config/realm.xml b/webapps/docs/config/realm.xml index 58407a11d..14b91e902 100644 --- a/webapps/docs/config/realm.xml +++ b/webapps/docs/config/realm.xml @@ -608,12 +608,68 @@ listed. Authentication against any Realm will be sufficient to authenticate the user.

-

The Combined Realm implementation does not support any additional +

The CombinedRealm implementation does not support any additional attributes.

See the Container-Managed Security Guide for more information on setting up container managed security - using the Combined Realm component.

+ using the CombinedRealm component.

+ +

LockOut Realm (org.apache.catalina.realm.LockOutRealm)

+ +

LockOutRealm is an implementation of the Tomcat 6 + Realm interface that extends the CombinedRealm to provide lock + out functionality to provide a user lock out mechanism if there are too many + failed authentication attempts in a given period of time.

+ +

To ensure correct operation, there is a reasonable degree of + synchronisation in this Realm.

+ +

This Realm does not require modification to the underlying Realms or the + associated user storage mecahisms. It achieves this by recording all failed + logins, including those for users that do not exist. To prevent a DOS by + deliberating making requests with invalid users (and hence causing this + cache to grow) the size of the list of users that have failed authentication + is limited.

+ +

Sub-realms are defined by nesting Realm elements inside the + Realm element that defines the LockOutRealm. Authentication + will be attempted against each Realm in the order they are + listed. Authentication against any Realm will be sufficient to authenticate + the user.

+ +

The LockOutRealm implementation supports the following additional + attributes.

+ + + + +

If a failed user is removed from the cache because the cache is too + big before it has been in the cache for at least this period of time (in + seconds) a warning message will be logged. Defaults to 3600 (1 hour).

+
+ + +

Number of users that have failed authentication to keep in cache. Over + time the cache will grow to this size and may not shrink. Defaults to + 1000.

+
+ + +

The number of times in a row a user has to fail authentication to be + locked out. Defaults to 5.

+
+ + +

The time (in seconds) a user is locked out for after too many + authentication failures. Defaults to 300 (5 minutes).

+
+ +
+ +

See the Container-Managed Security + Guide for more information on setting up container managed security + using the LockOutRealm component.

@@ -623,9 +679,10 @@
-

Combined Realm Implementation

+

CombinedRealm Implementation

-

If you are using the Combined Realm Implementation +

If you are using the CombinedRealm Implementation or a Realm + that extends the CombinedRealm, e.g. the LockOutRealm, <Realm> elements may be nested inside it.

Other Realm Implementations

diff --git a/webapps/docs/realm-howto.xml b/webapps/docs/realm-howto.xml index e2fdab74b..985e524af 100644 --- a/webapps/docs/realm-howto.xml +++ b/webapps/docs/realm-howto.xml @@ -55,6 +55,8 @@ Standard Realm Implementations
JNDIRealm
MemoryRealm
JAASRealm
+CombinedRealm
+LockOutRealm

@@ -1485,7 +1487,7 @@ and restarting the server, without any code changes to your application. the user.

Realm Element Attributes

-

To configure CombinedRealm, you create a <Realm> +

To configure a CombinedRealm, you create a <Realm> element and nest it in your $CATALINA_BASE/conf/server.xml file within your <Engine> or <Host>. You can also nest inside a <Context> node in a @@ -1520,6 +1522,85 @@ UserDatabase Realm and a DataSource Realm.

+ + +

Introduction

+ +

LockOutRealm is an implementation of the Tomcat 6 + Realm interface that extends the CombinedRealm to provide lock + out functionality to provide a user lock out mechanism if there are too many + failed authentication attempts in a given period of time.

+ +

To ensure correct operation, there is a reasonable degree of + synchronisation in this Realm.

+ +

This Realm does not require modification to the underlying Realms or the + associated user storage mecahisms. It achieves this by recording all failed + logins, including those for users that do not exist. To prevent a DOS by + deliberating making requests with invalid users (and hence causing this + cache to grow) the size of the list of users that have failed authentication + is limited.

+ +

Sub-realms are defined by nesting Realm elements inside the + Realm element that defines the LockOutRealm. Authentication + will be attempted against each Realm in the order they are + listed. Authentication against any Realm will be sufficient to authenticate + the user.

+ +

Realm Element Attributes

+

To configure a LockOutRealm, you create a <Realm> + element and nest it in your $CATALINA_BASE/conf/server.xml + file within your <Engine> or <Host>. + You can also nest inside a <Context> node in a + context.xml file. The following attributes are supported by + this implementation:

+ + + + +

The fully qualified Java class name of this Realm implementation. + You MUST specify the value + "org.apache.catalina.realm.LockOutRealm" here.

+
+ + +

If a failed user is removed from the cache because the cache is too + big before it has been in the cache for at least this period of time (in + seconds) a warning message will be logged. Defaults to 3600 (1 hour).

+
+ + +

Number of users that have failed authentication to keep in cache. Over + time the cache will grow to this size and may not shrink. Defaults to + 1000.

+
+ + +

The number of times in a row a user has to fail authentication to be + locked out. Defaults to 5.

+
+ + +

The time (in seconds) a user is locked out for after too many + authentication failures. Defaults to 300 (5 minutes).

+
+ +
+ +

Example

+ +

Here is an example of how your server.xml snippet should look to add lock out +functionality to a UserDatabase Realm.

+ + +<Realm className="org.apache.catalina.realm.LockOutRealm" > + <Realm className="org.apache.catalina.realm.UserDatabaseRealm" + resourceName="UserDatabase"/> +<Realm/> + + +
+