implement login attempt handler to handle failed authentications

This commit is contained in:
Sebastian Sdorra
2013-09-16 13:58:19 +02:00
parent 364ecfbdb8
commit bfa4372626
6 changed files with 436 additions and 16 deletions

View File

@@ -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;
}

View File

@@ -79,7 +79,9 @@ import sonia.scm.repository.RepositoryDAO;
import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryManagerProvider; import sonia.scm.repository.RepositoryManagerProvider;
import sonia.scm.repository.RepositoryProvider; import sonia.scm.repository.RepositoryProvider;
import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.repository.spi.HookEventFacade;
import sonia.scm.repository.xml.XmlRepositoryDAO; import sonia.scm.repository.xml.XmlRepositoryDAO;
import sonia.scm.resources.DefaultResourceManager; import sonia.scm.resources.DefaultResourceManager;
import sonia.scm.resources.DevelopmentResourceManager; import sonia.scm.resources.DevelopmentResourceManager;
@@ -87,10 +89,12 @@ import sonia.scm.resources.ResourceManager;
import sonia.scm.resources.ScriptResourceServlet; import sonia.scm.resources.ScriptResourceServlet;
import sonia.scm.security.CipherHandler; import sonia.scm.security.CipherHandler;
import sonia.scm.security.CipherUtil; import sonia.scm.security.CipherUtil;
import sonia.scm.security.ConfigurableLoginAttemptHandler;
import sonia.scm.security.DefaultKeyGenerator; import sonia.scm.security.DefaultKeyGenerator;
import sonia.scm.security.DefaultSecuritySystem; import sonia.scm.security.DefaultSecuritySystem;
import sonia.scm.security.EncryptionHandler; import sonia.scm.security.EncryptionHandler;
import sonia.scm.security.KeyGenerator; import sonia.scm.security.KeyGenerator;
import sonia.scm.security.LoginAttemptHandler;
import sonia.scm.security.MessageDigestEncryptionHandler; import sonia.scm.security.MessageDigestEncryptionHandler;
import sonia.scm.security.RepositoryPermissionResolver; import sonia.scm.security.RepositoryPermissionResolver;
import sonia.scm.security.SecurityContext; import sonia.scm.security.SecurityContext;
@@ -149,8 +153,6 @@ import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.Set; 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(WebSecurityContext.class).to(BasicSecurityContext.class);
bind(SecuritySystem.class).to(DefaultSecuritySystem.class); bind(SecuritySystem.class).to(DefaultSecuritySystem.class);
bind(AdministrationContext.class, DefaultAdministrationContext.class); bind(AdministrationContext.class, DefaultAdministrationContext.class);
bind(LoginAttemptHandler.class, ConfigurableLoginAttemptHandler.class);
// bind cache // bind cache
bind(CacheManager.class, GuavaCacheManager.class); bind(CacheManager.class, GuavaCacheManager.class);
@@ -328,7 +331,7 @@ public class ScmServletModule extends ServletModule
// bind repository service factory // bind repository service factory
bind(RepositoryServiceFactory.class); bind(RepositoryServiceFactory.class);
// bind new hook api // bind new hook api
bind(HookContextFactory.class); bind(HookContextFactory.class);
bind(HookEventFacade.class); bind(HookEventFacade.class);

View File

@@ -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<Object, LoginAttempt> attempts =
new ConcurrentHashMap<Object, LoginAttempt>();
/** Field description */
private final ScmConfiguration configuration;
}

View File

@@ -108,15 +108,11 @@ public class ScmRealm extends AuthorizingRealm
/** /**
* Constructs ... * Constructs ...
* *
*
*
* @param configuration * @param configuration
* @param securitySystem * @param loginAttemptHandler
* @param collector * @param collector
* @param cacheManager
* @param userManager * @param userManager
* @param groupManager * @param groupManager
* @param repositoryDAO
* @param userDAO * @param userDAO
* @param authenticator * @param authenticator
* @param manager * @param manager
@@ -125,6 +121,7 @@ public class ScmRealm extends AuthorizingRealm
*/ */
@Inject @Inject
public ScmRealm(ScmConfiguration configuration, public ScmRealm(ScmConfiguration configuration,
LoginAttemptHandler loginAttemptHandler,
AuthorizationCollector collector,UserManager userManager, AuthorizationCollector collector,UserManager userManager,
GroupManager groupManager, UserDAO userDAO, GroupManager groupManager, UserDAO userDAO,
AuthenticationManager authenticator, RepositoryManager manager, AuthenticationManager authenticator, RepositoryManager manager,
@@ -132,6 +129,7 @@ public class ScmRealm extends AuthorizingRealm
Provider<HttpServletResponse> responseProvider) Provider<HttpServletResponse> responseProvider)
{ {
this.configuration = configuration; this.configuration = configuration;
this.loginAttemptHandler = loginAttemptHandler;
this.collector = collector; this.collector = collector;
this.userManager = userManager; this.userManager = userManager;
this.groupManager = groupManager; this.groupManager = groupManager;
@@ -151,6 +149,8 @@ public class ScmRealm extends AuthorizingRealm
// set components // set components
setPermissionResolver(new RepositoryPermissionResolver()); setPermissionResolver(new RepositoryPermissionResolver());
} }
private final LoginAttemptHandler loginAttemptHandler;
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------
@@ -174,6 +174,8 @@ public class ScmRealm extends AuthorizingRealm
{ {
throw new UnsupportedTokenException("ScmAuthenticationToken is required"); throw new UnsupportedTokenException("ScmAuthenticationToken is required");
} }
loginAttemptHandler.beforeAuthentication(authToken);
UsernamePasswordToken token = (UsernamePasswordToken) authToken; UsernamePasswordToken token = (UsernamePasswordToken) authToken;
@@ -184,6 +186,7 @@ public class ScmRealm extends AuthorizingRealm
if ((result != null) && (AuthenticationState.SUCCESS == result.getState())) if ((result != null) && (AuthenticationState.SUCCESS == result.getState()))
{ {
loginAttemptHandler.onSuccessfulAuthentication(authToken, result);
info = createAuthenticationInfo(token, result); info = createAuthenticationInfo(token, result);
} }
else if ((result != null) else if ((result != null)
@@ -194,6 +197,7 @@ public class ScmRealm extends AuthorizingRealm
} }
else else
{ {
loginAttemptHandler.onUnsuccessfulAuthentication(authToken, result);
throw new AccountException("authentication failed"); throw new AccountException("authentication failed");
} }
@@ -532,26 +536,26 @@ public class ScmRealm extends AuthorizingRealm
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
/** Field description */ /** Field description */
private AuthenticationManager authenticator; private final AuthenticationManager authenticator;
/** Field description */ /** Field description */
private AuthorizationCollector collector; private final AuthorizationCollector collector;
/** Field description */ /** Field description */
private ScmConfiguration configuration; private final ScmConfiguration configuration;
/** Field description */ /** Field description */
private GroupManager groupManager; private final GroupManager groupManager;
/** Field description */ /** Field description */
private Provider<HttpServletRequest> requestProvider; private final Provider<HttpServletRequest> requestProvider;
/** Field description */ /** Field description */
private Provider<HttpServletResponse> responseProvider; private final Provider<HttpServletResponse> responseProvider;
/** Field description */ /** Field description */
private UserDAO userDAO; private final UserDAO userDAO;
/** Field description */ /** Field description */
private UserManager userManager; private final UserManager userManager;
} }

View File

@@ -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);
}
}

View File

@@ -41,6 +41,7 @@ import com.google.common.collect.ImmutableSet;
import com.google.inject.Provider; import com.google.inject.Provider;
import org.apache.shiro.authc.AccountException; import org.apache.shiro.authc.AccountException;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UnknownAccountException;
@@ -484,9 +485,25 @@ public class ScmRealmTest
securitySystem, securitySystem,
new RepositoryPermissionResolver() 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( return new ScmRealm(
new ScmConfiguration(), new ScmConfiguration(),
dummyLoginAttemptHandler,
collector, collector,
// cacheManager, // cacheManager,
userManager, userManager,