diff --git a/scm-core/src/main/java/sonia/scm/security/LoginAttemptHandler.java b/scm-core/src/main/java/sonia/scm/security/LoginAttemptHandler.java new file mode 100644 index 0000000000..9d44c3cfe7 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/LoginAttemptHandler.java @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.security; + +//~--- non-JDK imports -------------------------------------------------------- + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationToken; + +import sonia.scm.web.security.AuthenticationResult; + +/** + * Login attempt handler. + * + * @author Sebastian Sdorra + * @since 1.34 + */ +public interface LoginAttemptHandler +{ + + /** + * This method is called before the authentication procedure is invoked. + * + * + * @param token authentication token + * + * @throws AuthenticationException + */ + public void beforeAuthentication(AuthenticationToken token) + throws AuthenticationException; + + /** + * Handle successful authentication. + * + * + * @param token authentication token + * @param result successful authentication result + * + * @throws AuthenticationException + */ + public void onSuccessfulAuthentication(AuthenticationToken token, + AuthenticationResult result) + throws AuthenticationException; + + /** + * Handle unsuccessful authentication. + * + * + * @param token authentication token + * @param result unsuccessful authentication result + * + * @throws AuthenticationException + */ + public void onUnsuccessfulAuthentication(AuthenticationToken token, + AuthenticationResult result) + throws AuthenticationException; +} diff --git a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java index 2a1554d3a0..83c25b5b91 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java @@ -79,7 +79,9 @@ import sonia.scm.repository.RepositoryDAO; import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryManagerProvider; import sonia.scm.repository.RepositoryProvider; +import sonia.scm.repository.api.HookContextFactory; import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.repository.spi.HookEventFacade; import sonia.scm.repository.xml.XmlRepositoryDAO; import sonia.scm.resources.DefaultResourceManager; import sonia.scm.resources.DevelopmentResourceManager; @@ -87,10 +89,12 @@ import sonia.scm.resources.ResourceManager; import sonia.scm.resources.ScriptResourceServlet; import sonia.scm.security.CipherHandler; import sonia.scm.security.CipherUtil; +import sonia.scm.security.ConfigurableLoginAttemptHandler; import sonia.scm.security.DefaultKeyGenerator; import sonia.scm.security.DefaultSecuritySystem; import sonia.scm.security.EncryptionHandler; import sonia.scm.security.KeyGenerator; +import sonia.scm.security.LoginAttemptHandler; import sonia.scm.security.MessageDigestEncryptionHandler; import sonia.scm.security.RepositoryPermissionResolver; import sonia.scm.security.SecurityContext; @@ -149,8 +153,6 @@ import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; -import sonia.scm.repository.api.HookContextFactory; -import sonia.scm.repository.spi.HookEventFacade; /** * @@ -283,6 +285,7 @@ public class ScmServletModule extends ServletModule bind(WebSecurityContext.class).to(BasicSecurityContext.class); bind(SecuritySystem.class).to(DefaultSecuritySystem.class); bind(AdministrationContext.class, DefaultAdministrationContext.class); + bind(LoginAttemptHandler.class, ConfigurableLoginAttemptHandler.class); // bind cache bind(CacheManager.class, GuavaCacheManager.class); @@ -328,7 +331,7 @@ public class ScmServletModule extends ServletModule // bind repository service factory bind(RepositoryServiceFactory.class); - + // bind new hook api bind(HookContextFactory.class); bind(HookEventFacade.class); diff --git a/scm-webapp/src/main/java/sonia/scm/security/ConfigurableLoginAttemptHandler.java b/scm-webapp/src/main/java/sonia/scm/security/ConfigurableLoginAttemptHandler.java new file mode 100644 index 0000000000..8986d756de --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/ConfigurableLoginAttemptHandler.java @@ -0,0 +1,245 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.security; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.base.Objects; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.ExcessiveAttemptsException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import sonia.scm.config.ScmConfiguration; +import sonia.scm.web.security.AuthenticationResult; + +//~--- JDK imports ------------------------------------------------------------ + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; + +/** + * + * @author Sebastian Sdorra + */ +@Singleton +public class ConfigurableLoginAttemptHandler implements LoginAttemptHandler +{ + + /** + * the logger for ConfigurableLoginAttemptHandler + */ + private static final Logger logger = + LoggerFactory.getLogger(ConfigurableLoginAttemptHandler.class); + + //~--- constructors --------------------------------------------------------- + + /** + * Constructs ... + * + * + * @param configuration + */ + @Inject + public ConfigurableLoginAttemptHandler(ScmConfiguration configuration) + { + this.configuration = configuration; + } + + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + * + * @param token + * + * @throws AuthenticationException + */ + @Override + public void beforeAuthentication(AuthenticationToken token) + throws AuthenticationException + { + LoginAttempt attempt = getAttempt(token); + long time = System.currentTimeMillis() - attempt.lastAttempt; + + if (time > TimeUnit.SECONDS.toMillis(5l)) + { + logger.debug("reset login attempts for {}, because of time", + token.getPrincipal()); + attempt.reset(); + } + else if (attempt.counter >= 5) + { + logger.warn("account {} is temporary locked, because of {}", + token.getPrincipal(), attempt); + attempt.increase(); + + throw new ExcessiveAttemptsException("account is temporarly locked"); + } + } + + /** + * Method description + * + * + * @param token + * @param result + * + * @throws AuthenticationException + */ + @Override + public void onSuccessfulAuthentication(AuthenticationToken token, + AuthenticationResult result) + throws AuthenticationException + { + logger.debug("reset login attempts for {}, because of successful login", + token.getPrincipal()); + getAttempt(token).reset(); + } + + /** + * Method description + * + * + * @param token + * @param result + * + * @throws AuthenticationException + */ + @Override + public void onUnsuccessfulAuthentication(AuthenticationToken token, + AuthenticationResult result) + throws AuthenticationException + { + logger.debug("increase failed login attempts for {}", token.getPrincipal()); + + LoginAttempt attempt = getAttempt(token); + + attempt.increase(); + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Method description + * + * + * @param token + * + * @return + */ + private LoginAttempt getAttempt(AuthenticationToken token) + { + LoginAttempt freshAttempt = new LoginAttempt(); + LoginAttempt attempt = attempts.putIfAbsent(token.getPrincipal(), + freshAttempt); + + if (attempt == null) + { + attempt = freshAttempt; + } + + return attempt; + } + + //~--- inner classes -------------------------------------------------------- + + /** + * Login attempt + */ + private static class LoginAttempt + { + + /** + * Method description + * + * + * @return + */ + @Override + public String toString() + { + //J- + return Objects.toStringHelper(this) + .add("counter", counter) + .add("lastAttempt", lastAttempt) + .toString(); + //J+ + } + + /** + * Method description + * + */ + synchronized void increase() + { + counter++; + lastAttempt = System.currentTimeMillis(); + } + + /** + * Method description + * + */ + synchronized void reset() + { + lastAttempt = -1l; + counter = 0; + } + + //~--- fields ------------------------------------------------------------- + + /** Field description */ + private int counter = 0; + + /** Field description */ + private long lastAttempt = -1l; + } + + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + private final ConcurrentMap attempts = + new ConcurrentHashMap(); + + /** Field description */ + private final ScmConfiguration configuration; +} diff --git a/scm-webapp/src/main/java/sonia/scm/security/ScmRealm.java b/scm-webapp/src/main/java/sonia/scm/security/ScmRealm.java index 1b42a5e1ec..1234c4eabd 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/ScmRealm.java +++ b/scm-webapp/src/main/java/sonia/scm/security/ScmRealm.java @@ -108,15 +108,11 @@ public class ScmRealm extends AuthorizingRealm /** * Constructs ... * - * - * * @param configuration - * @param securitySystem + * @param loginAttemptHandler * @param collector - * @param cacheManager * @param userManager * @param groupManager - * @param repositoryDAO * @param userDAO * @param authenticator * @param manager @@ -125,6 +121,7 @@ public class ScmRealm extends AuthorizingRealm */ @Inject public ScmRealm(ScmConfiguration configuration, + LoginAttemptHandler loginAttemptHandler, AuthorizationCollector collector,UserManager userManager, GroupManager groupManager, UserDAO userDAO, AuthenticationManager authenticator, RepositoryManager manager, @@ -132,6 +129,7 @@ public class ScmRealm extends AuthorizingRealm Provider responseProvider) { this.configuration = configuration; + this.loginAttemptHandler = loginAttemptHandler; this.collector = collector; this.userManager = userManager; this.groupManager = groupManager; @@ -151,6 +149,8 @@ public class ScmRealm extends AuthorizingRealm // set components setPermissionResolver(new RepositoryPermissionResolver()); } + + private final LoginAttemptHandler loginAttemptHandler; //~--- methods -------------------------------------------------------------- @@ -174,6 +174,8 @@ public class ScmRealm extends AuthorizingRealm { throw new UnsupportedTokenException("ScmAuthenticationToken is required"); } + + loginAttemptHandler.beforeAuthentication(authToken); UsernamePasswordToken token = (UsernamePasswordToken) authToken; @@ -184,6 +186,7 @@ public class ScmRealm extends AuthorizingRealm if ((result != null) && (AuthenticationState.SUCCESS == result.getState())) { + loginAttemptHandler.onSuccessfulAuthentication(authToken, result); info = createAuthenticationInfo(token, result); } else if ((result != null) @@ -194,6 +197,7 @@ public class ScmRealm extends AuthorizingRealm } else { + loginAttemptHandler.onUnsuccessfulAuthentication(authToken, result); throw new AccountException("authentication failed"); } @@ -532,26 +536,26 @@ public class ScmRealm extends AuthorizingRealm //~--- fields --------------------------------------------------------------- /** Field description */ - private AuthenticationManager authenticator; + private final AuthenticationManager authenticator; /** Field description */ - private AuthorizationCollector collector; + private final AuthorizationCollector collector; /** Field description */ - private ScmConfiguration configuration; + private final ScmConfiguration configuration; /** Field description */ - private GroupManager groupManager; + private final GroupManager groupManager; /** Field description */ - private Provider requestProvider; + private final Provider requestProvider; /** Field description */ - private Provider responseProvider; + private final Provider responseProvider; /** Field description */ - private UserDAO userDAO; + private final UserDAO userDAO; /** Field description */ - private UserManager userManager; + private final UserManager userManager; } diff --git a/scm-webapp/src/test/java/sonia/scm/security/ConfigurableLoginAttemptHandlerTest.java b/scm-webapp/src/test/java/sonia/scm/security/ConfigurableLoginAttemptHandlerTest.java new file mode 100644 index 0000000000..a7d773b4f3 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/security/ConfigurableLoginAttemptHandlerTest.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ +package sonia.scm.security; + +import java.util.concurrent.TimeUnit; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.junit.Test; +import sonia.scm.web.security.AuthenticationResult; + +/** + * + * @author Sebastian Sdorra + */ +public class ConfigurableLoginAttemptHandlerTest +{ + + @Test + public void testLoginAttempt() throws InterruptedException + { + ConfigurableLoginAttemptHandler handler = new ConfigurableLoginAttemptHandler(null); + UsernamePasswordToken token = new UsernamePasswordToken("hansolo", "hobbo"); + 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); + handler.beforeAuthentication(token); + handler.onUnsuccessfulAuthentication(token, AuthenticationResult.FAILED); + // asd + Thread.currentThread().sleep(TimeUnit.SECONDS.toMillis(10)); + handler.beforeAuthentication(token); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/security/ScmRealmTest.java b/scm-webapp/src/test/java/sonia/scm/security/ScmRealmTest.java index af441780ae..f8c7efbe3e 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/ScmRealmTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/ScmRealmTest.java @@ -41,6 +41,7 @@ import com.google.common.collect.ImmutableSet; import com.google.inject.Provider; import org.apache.shiro.authc.AccountException; +import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.UnknownAccountException; @@ -484,9 +485,25 @@ public class ScmRealmTest securitySystem, new RepositoryPermissionResolver() ); + + LoginAttemptHandler dummyLoginAttemptHandler = new LoginAttemptHandler() + { + @Override + public void beforeAuthentication(AuthenticationToken token) + throws AuthenticationException {} + + @Override + public void onSuccessfulAuthentication(AuthenticationToken token, + AuthenticationResult result) throws AuthenticationException {} + + @Override + public void onUnsuccessfulAuthentication(AuthenticationToken token, + AuthenticationResult result) throws AuthenticationException {} + }; return new ScmRealm( new ScmConfiguration(), + dummyLoginAttemptHandler, collector, // cacheManager, userManager,