diff --git a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java index 928039a4f1..9fb6d6f8bc 100644 --- a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java +++ b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java @@ -52,6 +52,7 @@ import java.io.File; import java.util.Collection; import java.util.HashSet; import java.util.Set; +import java.util.concurrent.TimeUnit; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; @@ -152,7 +153,6 @@ public class ScmConfiguration */ public void load(ScmConfiguration other) { - this.servername = other.servername; this.dateFormat = other.dateFormat; this.pluginUrl = other.pluginUrl; this.anonymousAccessEnabled = other.anonymousAccessEnabled; @@ -168,8 +168,11 @@ public class ScmConfiguration this.baseUrl = other.baseUrl; this.disableGroupingGrid = other.disableGroupingGrid; this.enableRepositoryArchive = other.enableRepositoryArchive; + this.loginAttemptLimit = other.loginAttemptLimit; + this.loginAttemptLimitTimeout = other.loginAttemptLimitTimeout; // deprecated fields + this.servername = other.servername; this.sslPort = other.sslPort; this.enableSSL = other.enableSSL; this.enablePortForward = other.enablePortForward; @@ -227,7 +230,7 @@ public class ScmConfiguration /** * Returns the date format for the user interface. This format is a * JavaScript date format, from the library moment.js. - * + * * @see http://momentjs.com/docs/#/parsing/ * @return moment.js date format */ @@ -249,6 +252,31 @@ public class ScmConfiguration return forwardPort; } + /** + * Returns maximum allowed login attempts. + * + * @return maximum allowed login attempts + * + * @since 1.34 + */ + public int getLoginAttemptLimit() + { + return loginAttemptLimit; + } + + /** + * Returns the timeout in seconds for users which are temporary disabled, + * because of too many failed login attempts. + * + * @return login attempt timeout in seconds + * + * @since 1.34 + */ + public long getLoginAttemptLimitTimeout() + { + return loginAttemptLimitTimeout; + } + /** * Returns the url of the plugin repository. This url can contain placeholders. * Explanation of the {placeholders}: @@ -581,6 +609,32 @@ public class ScmConfiguration this.forwardPort = forwardPort; } + /** + * Set maximum allowed login attempts. + * + * + * @param loginAttemptLimit login attempt limit + * + * @since 1.34 + */ + public void setLoginAttemptLimit(int loginAttemptLimit) + { + this.loginAttemptLimit = loginAttemptLimit; + } + + /** + * Sets the timeout in seconds for users which are temporary disabled, + * because of too many failed login attempts. + * + * @param loginAttemptLimitTimeout login attempt timeout in seconds + * + * @since 1.34 + */ + public void setLoginAttemptLimitTimeout(long loginAttemptLimitTimeout) + { + this.loginAttemptLimitTimeout = loginAttemptLimitTimeout; + } + /** * Method description * @@ -692,9 +746,6 @@ public class ScmConfiguration @XmlElement(name = "base-url") private String baseUrl; - /** Field description */ - private boolean enableProxy = false; - /** Field description */ @XmlElement(name = "force-base-url") private boolean forceBaseUrl; @@ -703,6 +754,24 @@ public class ScmConfiguration @Deprecated private int forwardPort = 80; + /** + * Maximum allowed login attempts. + * + * @since 1.34 + */ + @XmlElement(name = "login-attempt-limit") + private int loginAttemptLimit = -1; + + /** + * Login attempt timeout. + * + * @since 1.34 + */ + private long loginAttemptLimitTimeout = TimeUnit.MINUTES.toSeconds(5l); + + /** Field description */ + private boolean enableProxy = false; + /** Field description */ @XmlElement(name = "plugin-url") private String pluginUrl = DEFAULT_PLUGINURL; diff --git a/scm-webapp/src/main/java/sonia/scm/security/ConfigurableLoginAttemptHandler.java b/scm-webapp/src/main/java/sonia/scm/security/ConfigurableLoginAttemptHandler.java index 8986d756de..8cd9b843f9 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/ConfigurableLoginAttemptHandler.java +++ b/scm-webapp/src/main/java/sonia/scm/security/ConfigurableLoginAttemptHandler.java @@ -95,17 +95,83 @@ public class ConfigurableLoginAttemptHandler implements LoginAttemptHandler @Override public void beforeAuthentication(AuthenticationToken token) throws AuthenticationException + { + if (isEnabled()) + { + handleBeforeAuthentication(token); + } + else + { + logger.trace("LoginAttemptHandler is disabled"); + } + } + + /** + * Method description + * + * + * @param token + * @param result + * + * @throws AuthenticationException + */ + @Override + public void onSuccessfulAuthentication(AuthenticationToken token, + AuthenticationResult result) + throws AuthenticationException + { + if (isEnabled()) + { + handleOnSuccessfulAuthentication(token); + } + else + { + logger.trace("LoginAttemptHandler is disabled"); + } + } + + /** + * Method description + * + * + * @param token + * @param result + * + * @throws AuthenticationException + */ + @Override + public void onUnsuccessfulAuthentication(AuthenticationToken token, + AuthenticationResult result) + throws AuthenticationException + { + if (isEnabled()) + { + handleOnUnsuccessfulAuthentication(token); + } + else + { + logger.trace("LoginAttemptHandler is disabled"); + } + } + + /** + * Method description + * + * + * @param token + */ + private void handleBeforeAuthentication(AuthenticationToken token) { LoginAttempt attempt = getAttempt(token); long time = System.currentTimeMillis() - attempt.lastAttempt; - if (time > TimeUnit.SECONDS.toMillis(5l)) + if (time > getLoginAttemptLimitTimeout()) { logger.debug("reset login attempts for {}, because of time", token.getPrincipal()); attempt.reset(); } - else if (attempt.counter >= 5) + else if (attempt.counter >= configuration.getLoginAttemptLimit()) { logger.warn("account {} is temporary locked, because of {}", token.getPrincipal(), attempt); @@ -124,9 +190,7 @@ public class ConfigurableLoginAttemptHandler implements LoginAttemptHandler * * @throws AuthenticationException */ - @Override - public void onSuccessfulAuthentication(AuthenticationToken token, - AuthenticationResult result) + private void handleOnSuccessfulAuthentication(AuthenticationToken token) throws AuthenticationException { logger.debug("reset login attempts for {}, because of successful login", @@ -143,9 +207,7 @@ public class ConfigurableLoginAttemptHandler implements LoginAttemptHandler * * @throws AuthenticationException */ - @Override - public void onUnsuccessfulAuthentication(AuthenticationToken token, - AuthenticationResult result) + private void handleOnUnsuccessfulAuthentication(AuthenticationToken token) throws AuthenticationException { logger.debug("increase failed login attempts for {}", token.getPrincipal()); @@ -179,6 +241,30 @@ public class ConfigurableLoginAttemptHandler implements LoginAttemptHandler return attempt; } + /** + * Method description + * + * + * @return + */ + private long getLoginAttemptLimitTimeout() + { + return TimeUnit.SECONDS.toMillis( + configuration.getLoginAttemptLimitTimeout()); + } + + /** + * Method description + * + * + * @return + */ + private boolean isEnabled() + { + return (configuration.getLoginAttemptLimit() > 0) + && (configuration.getLoginAttemptLimitTimeout() > 0l); + } + //~--- inner classes -------------------------------------------------------- /** diff --git a/scm-webapp/src/test/java/sonia/scm/security/ConfigurableLoginAttemptHandlerTest.java b/scm-webapp/src/test/java/sonia/scm/security/ConfigurableLoginAttemptHandlerTest.java index a7d773b4f3..ea1fb7a6f2 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/ConfigurableLoginAttemptHandlerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/ConfigurableLoginAttemptHandlerTest.java @@ -27,12 +27,25 @@ * http://bitbucket.org/sdorra/scm-manager * */ + + + package sonia.scm.security; -import java.util.concurrent.TimeUnit; +//~--- non-JDK imports -------------------------------------------------------- + +import org.apache.shiro.authc.ExcessiveAttemptsException; import org.apache.shiro.authc.UsernamePasswordToken; + import org.junit.Test; + +import sonia.scm.config.ScmConfiguration; import sonia.scm.web.security.AuthenticationResult; +import sonia.scm.web.security.AuthenticationState; + +//~--- JDK imports ------------------------------------------------------------ + +import java.util.concurrent.TimeUnit; /** * @@ -40,25 +53,87 @@ import sonia.scm.web.security.AuthenticationResult; */ public class ConfigurableLoginAttemptHandlerTest { - - @Test - public void testLoginAttempt() throws InterruptedException + + /** + * Method description + * + */ + @Test(expected = ExcessiveAttemptsException.class) + public void testLoginAttemptLimitReached() { - ConfigurableLoginAttemptHandler handler = new ConfigurableLoginAttemptHandler(null); + LoginAttemptHandler handler = createHandler(2, 2); UsernamePasswordToken token = new UsernamePasswordToken("hansolo", "hobbo"); + handler.beforeAuthentication(token); handler.onUnsuccessfulAuthentication(token, AuthenticationResult.FAILED); - handler.beforeAuthentication(token); + handler.beforeAuthentication(token); handler.onUnsuccessfulAuthentication(token, AuthenticationResult.FAILED); - handler.beforeAuthentication(token); - handler.onUnsuccessfulAuthentication(token, AuthenticationResult.FAILED); - handler.beforeAuthentication(token); - handler.onUnsuccessfulAuthentication(token, AuthenticationResult.FAILED); - handler.beforeAuthentication(token); - handler.onUnsuccessfulAuthentication(token, AuthenticationResult.FAILED); - // asd - Thread.currentThread().sleep(TimeUnit.SECONDS.toMillis(10)); handler.beforeAuthentication(token); } - + + /** + * Method description + * + * + * @throws InterruptedException + */ + @Test + public void testLoginAttemptLimitTimeout() throws InterruptedException + { + LoginAttemptHandler handler = createHandler(2, 1); + UsernamePasswordToken token = new UsernamePasswordToken("hansolo", "hobbo"); + + handler.beforeAuthentication(token); + handler.onUnsuccessfulAuthentication(token, AuthenticationResult.FAILED); + handler.beforeAuthentication(token); + handler.onUnsuccessfulAuthentication(token, AuthenticationResult.FAILED); + Thread.currentThread().sleep(TimeUnit.MILLISECONDS.toMillis(1200l)); + handler.beforeAuthentication(token); + } + + /** + * Method description + * + * + * @throws InterruptedException + */ + @Test + public void testLoginAttemptResetOnSuccess() throws InterruptedException + { + LoginAttemptHandler handler = createHandler(2, 1); + UsernamePasswordToken token = new UsernamePasswordToken("hansolo", "hobbo"); + + handler.beforeAuthentication(token); + handler.onUnsuccessfulAuthentication(token, AuthenticationResult.FAILED); + handler.beforeAuthentication(token); + handler.onUnsuccessfulAuthentication(token, AuthenticationResult.FAILED); + + handler.onSuccessfulAuthentication(token, + new AuthenticationResult(AuthenticationState.SUCCESS)); + + handler.beforeAuthentication(token); + handler.onUnsuccessfulAuthentication(token, AuthenticationResult.FAILED); + handler.beforeAuthentication(token); + handler.onUnsuccessfulAuthentication(token, AuthenticationResult.FAILED); + } + + /** + * Method description + * + * + * @param loginAttemptLimit + * @param loginAttemptLimitTimeout + * + * @return + */ + private LoginAttemptHandler createHandler(int loginAttemptLimit, + long loginAttemptLimitTimeout) + { + ScmConfiguration configuration = new ScmConfiguration(); + + configuration.setLoginAttemptLimit(loginAttemptLimit); + configuration.setLoginAttemptLimitTimeout(loginAttemptLimitTimeout); + + return new ConfigurableLoginAttemptHandler(configuration); + } }