From b458a61acae1e7b5ba14920410648b878ea3bfa4 Mon Sep 17 00:00:00 2001 From: jensp Date: Tue, 12 Apr 2016 17:54:40 +0000 Subject: [PATCH] CCM NG: The OneTimeAuthToken is now saved as hash in the database. git-svn-id: https://svn.libreccm.org/ccm/ccm_ng@3985 8810af33-2d31-482b-a856-94f89814c4df --- .../libreccm/security/OneTimeAuthManager.java | 106 +++++++++++++++--- .../org/libreccm/security/UserManager.java | 9 +- .../security/OneTimeAuthManagerTest.java | 3 +- 3 files changed, 97 insertions(+), 21 deletions(-) diff --git a/ccm-core/src/main/java/org/libreccm/security/OneTimeAuthManager.java b/ccm-core/src/main/java/org/libreccm/security/OneTimeAuthManager.java index 2d8440fe5..171550823 100644 --- a/ccm-core/src/main/java/org/libreccm/security/OneTimeAuthManager.java +++ b/ccm-core/src/main/java/org/libreccm/security/OneTimeAuthManager.java @@ -18,6 +18,8 @@ */ package org.libreccm.security; +import com.arsdigita.kernel.security.SecurityConfig; + import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.Date; @@ -32,6 +34,15 @@ import javax.persistence.TypedQuery; import javax.transaction.Transactional; import org.apache.commons.lang.RandomStringUtils; +import org.apache.shiro.authc.credential.PasswordMatcher; +import org.apache.shiro.authc.credential.PasswordService; +import org.apache.shiro.crypto.SecureRandomNumberGenerator; +import org.apache.shiro.crypto.hash.SimpleHash; +import org.apache.shiro.crypto.hash.format.DefaultHashFormatFactory; +import org.apache.shiro.crypto.hash.format.HashFormat; +import org.apache.shiro.crypto.hash.format.HashFormatFactory; +import org.apache.shiro.crypto.hash.format.Shiro1CryptFormat; +import org.apache.shiro.util.ByteSource; /** * This class manages the generation and delation of {@link OneTimeAuthToken}s. @@ -45,19 +56,26 @@ public class OneTimeAuthManager { private EntityManager entityManager; @Inject - private ConfigurationManager configurationManager; + private ConfigurationManager confManager; /** - * Creates a new one time auth token which for the provided user and the - * provided purpose. The length of the token and how long the token is valid - * are configured by the {@link OneTimeAuthConfig}. - * + * Creates a new one time authentication token which for the provided user + * and the provided purpose. The length of the token and how long the token + * is valid are configured by the {@link OneTimeAuthConfig}. + * * This method generates the token and saves it in the database. + * + * Please note: The returned token contains the not hashed + * authentication key/token string. The string is saved as hash in the + * database (using the same hash algorithm as for the passwords). This + * allows the caller to put the token string into an email (or an other + * message) and send it to the user. * - * @param user The user for which the one time auth token is generated. + * @param user The user for which the one time authentication token is + * generated. * @param purpose The purpose for which the token is generated. * - * @return The one time auth token. + * @return The one time authentication token with the not hashed token. */ @Transactional(Transactional.TxType.REQUIRED) public OneTimeAuthToken createForUser( @@ -68,26 +86,52 @@ public class OneTimeAuthManager { + "time auth token."); } - final OneTimeAuthConfig config = configurationManager.findConfiguration( + final OneTimeAuthConfig config = confManager.findConfiguration( OneTimeAuthConfig.class); - final OneTimeAuthToken token = new OneTimeAuthToken(); - token.setUser(user); - token.setPurpose(purpose); + // Token with cleartext token string + final OneTimeAuthToken tmpToken = new OneTimeAuthToken(); + tmpToken.setUser(user); + tmpToken.setPurpose(purpose); final String tokenStr = RandomStringUtils.randomAlphanumeric(config. getTokenLength()); - token.setToken(tokenStr); + tmpToken.setToken(tokenStr); final LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC); final LocalDateTime valid = now.plusSeconds(config.getTokenValid()); final Date validUntil = Date.from(valid.toInstant(ZoneOffset.UTC)); + tmpToken.setValidUntil(validUntil); + + final OneTimeAuthToken token = new OneTimeAuthToken(); + token.setUser(user); + token.setPurpose(purpose); token.setValidUntil(validUntil); + //Get configuration values for hashing from SecurityConfig + final SecurityConfig securityConfig = confManager.findConfiguration( + SecurityConfig.class); + final String hashAlgo = securityConfig.getHashAlgorithm(); + final int iterations = securityConfig.getHashIterations(); + + //Create the hash using Shiro's SimpleHash class + final SimpleHash hash = new SimpleHash(hashAlgo, + tokenStr.toCharArray(), + generateSalt(), + iterations); + //We want to use the Shiro1 format for storing the password. This + //format includes the algorithm used, the salt, the number of + //iterations used and the hashed password in special formatted string. + final HashFormatFactory hashFormatFactory + = new DefaultHashFormatFactory(); + final HashFormat hashFormat = hashFormatFactory.getInstance( + Shiro1CryptFormat.class.getName()); + token.setToken(hashFormat.format(hash)); + entityManager.persist(token); - return token; + return tmpToken; } /** @@ -191,7 +235,17 @@ public class OneTimeAuthManager { return false; } - return token.getToken().equals(submittedToken); + //The token is stored as hash (like the passwords. We + //the facilities provided by Shiro to verify the submitted token + //Create a new Shiro PasswordMatcher instance + final PasswordMatcher matcher = new PasswordMatcher(); + //Get the PasswordService instance from the matcher (the PasswordService + //class provides the methods we need here). + final PasswordService service = matcher.getPasswordService(); + + return service.passwordsMatch(submittedToken, token.getToken()); + +// return token.getToken().equals(submittedToken); } /** @@ -208,8 +262,30 @@ public class OneTimeAuthManager { //Ensure that we have a none detached instance final OneTimeAuthToken delete = entityManager.find( OneTimeAuthToken.class, token.getTokenId()); - + entityManager.remove(delete); } + /** + * Helper method for generating a random salt. The length of the generated + * salt is configured in the {@link LegacySecurityConfig}. + * + * @return A new random salt. + */ + private ByteSource generateSalt() { + final SecurityConfig securityConfig = confManager.findConfiguration( + SecurityConfig.class); + final int generatedSaltSize = securityConfig.getSaltLength(); + + if (generatedSaltSize % 8 != 0) { + throw new IllegalArgumentException( + "Salt length is not a multipe of 8"); + } + + final SecureRandomNumberGenerator generator + = new SecureRandomNumberGenerator(); + final int byteSize = generatedSaltSize / 8; //generatedSaltSize is in *bits* - convert to byte size: + return generator.nextBytes(byteSize); + } + } diff --git a/ccm-core/src/main/java/org/libreccm/security/UserManager.java b/ccm-core/src/main/java/org/libreccm/security/UserManager.java index 54c61c4ee..235d5952a 100644 --- a/ccm-core/src/main/java/org/libreccm/security/UserManager.java +++ b/ccm-core/src/main/java/org/libreccm/security/UserManager.java @@ -134,9 +134,10 @@ public class UserManager { * @return The hashed password.b */ private String hashPassword(final String password) { - //Get the values from the LegacySecurityConfig - final String hashAlgo = SecurityConfig.getConfig().getHashAlgorithm(); - final int iterations = SecurityConfig.getConfig().getHashIterations(); + //Get the values from the SecurityConfig + final SecurityConfig securityConfig = SecurityConfig.getConfig(); + final String hashAlgo = securityConfig.getHashAlgorithm(); + final int iterations = securityConfig.getHashIterations(); //Create the hash using Shiro's SimpleHash class final SimpleHash hash = new SimpleHash(hashAlgo, @@ -145,7 +146,7 @@ public class UserManager { iterations); //We want to use the Shiro1 format for storing the password. This - //format includes the algorithm used, the salt and the number of + //format includes the algorithm used, the salt, the number of //iterations used and the hashed password in special formatted string. final HashFormatFactory hashFormatFactory = new DefaultHashFormatFactory(); diff --git a/ccm-core/src/test/java/org/libreccm/security/OneTimeAuthManagerTest.java b/ccm-core/src/test/java/org/libreccm/security/OneTimeAuthManagerTest.java index 3dc8a2c2d..4ad8771a9 100644 --- a/ccm-core/src/test/java/org/libreccm/security/OneTimeAuthManagerTest.java +++ b/ccm-core/src/test/java/org/libreccm/security/OneTimeAuthManagerTest.java @@ -23,10 +23,8 @@ import java.time.LocalDateTime; import java.time.ZoneOffset; import java.time.temporal.ChronoUnit; import java.util.Date; -import java.util.Optional; import javax.inject.Inject; -import javax.persistence.EntityManager; import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.container.test.api.ShouldThrowException; @@ -126,6 +124,7 @@ public class OneTimeAuthManagerTest { getPackage()) .addPackage(org.libreccm.jpa.utils.MimeTypeConverter.class. getPackage()) + .addClass(com.arsdigita.kernel.security.SecurityConfig.class) .addPackage(org.libreccm.testutils.EqualsVerifier.class. getPackage()) .addPackage(org.libreccm.tests.categories.IntegrationTest.class.