CCM NG: UserManager finished

git-svn-id: https://svn.libreccm.org/ccm/ccm_ng@3512 8810af33-2d31-482b-a856-94f89814c4df
pull/2/head
jensp 2015-07-01 16:10:26 +00:00
parent e622668b1b
commit 31763e8a3c
8 changed files with 572 additions and 38 deletions

View File

@ -70,6 +70,10 @@
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>oro</groupId>
@ -262,6 +266,21 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jboss.tattletale</groupId>
<artifactId>tattletale-maven</artifactId>
<executions>
<execution>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
<configuration>
<source>${project.build.directory}</source>
<destination>${project.build.directory}/tattletale</destination>
</configuration>
</plugin>
</plugins>
</build>

View File

@ -18,7 +18,6 @@
*/
package org.libreccm.core;
import org.hibernate.validator.cdi.HibernateValidator;
import java.util.List;
@ -28,7 +27,6 @@ import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import javax.validation.Validator;
/**
* A base class providing common method needed by every repository.

View File

@ -26,6 +26,7 @@ package org.libreccm.core;
public final class CoreConstants {
public static final String CORE_XML_NS = "http://core.libreccm.org";
public static final String UTF8 = "UTF-8";
private CoreConstants() {
//Nothing

View File

@ -0,0 +1,67 @@
/*
* Copyright (C) 2015 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.libreccm.core;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
public class PasswordHashingFailedException extends RuntimeException {
private static final long serialVersionUID = 1L;
/**
* Creates a new instance of <code>PasswordHashingFailed</code> without detail message.
*/
public PasswordHashingFailedException() {
super();
}
/**
* Constructs an instance of <code>PasswordHashingFailed</code> with the specified detail message.
*
* @param msg The detail message.
*/
public PasswordHashingFailedException(final String msg) {
super(msg);
}
/**
* Constructs an instance of <code>PasswordHashingFailed</code> which wraps the
* specified exception.
*
* @param exception The exception to wrap.
*/
public PasswordHashingFailedException(final Exception exception) {
super(exception);
}
/**
* Constructs an instance of <code>PasswordHashingFailed</code> with the specified message which also wraps the
* specified exception.
*
* @param msg The detail message.
* @param exception The exception to wrap.
*/
public PasswordHashingFailedException(final String msg, final Exception exception) {
super(msg, exception);
}
}

View File

@ -18,6 +18,9 @@
*/
package org.libreccm.core;
import static org.libreccm.core.CoreConstants.*;
import org.apache.commons.codec.binary.Base64;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
@ -44,47 +47,107 @@ public class UserManager {
@Inject
private transient UserRepository userRepository;
private byte[] generateHash(final byte[] password, final byte[] salt) {
final byte[] saltedPassword = new byte[password.length + salt.length];
System.arraycopy(password, 0, saltedPassword, 0, password.length);
System.arraycopy(salt, 0, saltedPassword, password.length, salt.length);
try {
final MessageDigest digest = MessageDigest.getInstance(
getHashAlgorithm());
final byte[] hashedPassword = digest.digest(saltedPassword);
return hashedPassword;
} catch (NoSuchAlgorithmException ex) {
throw new PasswordHashingFailedException(
"Failed to generate hash for password", ex);
}
}
/**
* Update the password of an user.
*
* @param user The user whose password is to be updated.
* @param user The user whose password is to be updated.
* @param password The new password.
*/
public void updatePassword(final User user, final String password) {
try {
final Random random = new Random(System.currentTimeMillis());
final byte[] passwordBytes = password.getBytes("UTF-8");
final byte[] passwordBytes = password.getBytes(UTF8);
final byte[] salt = new byte[getSaltLength()];
random.nextBytes(salt);
final byte[] saltedPassword = new byte[passwordBytes.length
+ salt.length];
System.arraycopy(passwordBytes,
0,
saltedPassword,
0,
passwordBytes.length);
System.arraycopy(salt,
0,
saltedPassword,
passwordBytes.length,
salt.length);
final MessageDigest digest = MessageDigest.getInstance(
getHashAlgorithm());
final byte[] hashedBytes = digest.digest(saltedPassword);
final byte[] hashedBytes = generateHash(passwordBytes, salt);
final String hashedPassword = new String(hashedBytes, "UTF-8");
final Base64 base64 = new Base64();
final String hashedPassword = base64.encodeToString(hashedBytes);
final String saltStr = base64.encodeToString(salt);
user.setPassword(hashedPassword);
user.setSalt(saltStr);
userRepository.save(user);
} catch (NoSuchAlgorithmException ex) {
throw new RuntimeException(String.format(
"Configured hash algorithm '%s 'is not available.",
getHashAlgorithm()), ex);
} catch (UnsupportedEncodingException ex) {
throw new RuntimeException("UTF-8 charset is not supported.");
throw new PasswordHashingFailedException(
"UTF-8 charset is not supported.", ex);
}
}
/**
* Verify a password for the a specific user.
*
* @param user The user whose password is to be checked.
* @param password The password to verify.
*
* @return {@code true} if the provided password matches the password
* stored, {@code false} if not.
*/
public boolean verifyPasswordForUser(final User user,
final String password) {
final Base64 base64 = new Base64();
try {
final byte[] hashed = generateHash(
password.getBytes(UTF8), base64.decode(user.getSalt()));
final String hashedPassword = base64.encodeAsString(hashed);
return hashedPassword.equals(user.getPassword());
} catch (UnsupportedEncodingException ex) {
throw new PasswordHashingFailedException(
"Failed to generate hash of password", ex);
}
}
public boolean verifyPasswordForScreenname(final String screenname,
final String password)
throws UserNotFoundException {
final User user = userRepository.findByScreenName(screenname);
if (user == null) {
throw new UserNotFoundException(String.format(
"No user identified by screenname '%s' found.", screenname));
} else {
return verifyPasswordForUser(user, password);
}
}
public boolean verifyPasswordForEmail(final String emailAddress,
final String password)
throws UserNotFoundException{
final User user = userRepository.findByEmailAddress(emailAddress);
if (user == null) {
throw new UserNotFoundException(String.format(
"No user identified by email address '%s' found.", emailAddress));
} else {
return verifyPasswordForUser(user, password);
}
}
@ -95,7 +158,7 @@ public class UserManager {
*
* @return At the moment SHA-512, will be made configurable.
*/
private String getHashAlgorithm() {
public String getHashAlgorithm() {
return "SHA-512";
}
@ -106,7 +169,7 @@ public class UserManager {
*
* @return At the moment 256. Will be made configurable.
*/
private int getSaltLength() {
public int getSaltLength() {
return 256;
}

View File

@ -0,0 +1,67 @@
/*
* Copyright (C) 2015 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.libreccm.core;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
public class UserNotFoundException extends Exception {
private static final long serialVersionUID = 1L;
/**
* Creates a new instance of <code>UserNotFoundException</code> without detail message.
*/
public UserNotFoundException() {
super();
}
/**
* Constructs an instance of <code>UserNotFoundException</code> with the specified detail message.
*
* @param msg The detail message.
*/
public UserNotFoundException(final String msg) {
super(msg);
}
/**
* Constructs an instance of <code>UserNotFoundException</code> which wraps the
* specified exception.
*
* @param exception The exception to wrap.
*/
public UserNotFoundException(final Exception exception) {
super(exception);
}
/**
* Constructs an instance of <code>UserNotFoundException</code> with the specified message which also wraps the
* specified exception.
*
* @param msg The detail message.
* @param exception The exception to wrap.
*/
public UserNotFoundException(final String msg, final Exception exception) {
super(msg, exception);
}
}

View File

@ -0,0 +1,288 @@
/*
* Copyright (C) 2015 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.libreccm.core;
import static org.hamcrest.Matchers.*;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.arquillian.junit.InSequence;
import org.jboss.arquillian.persistence.PersistenceTest;
import org.jboss.arquillian.persistence.UsingDataSet;
import org.jboss.arquillian.transaction.api.annotation.TransactionMode;
import org.jboss.arquillian.transaction.api.annotation.Transactional;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.jboss.shrinkwrap.resolver.api.maven.Maven;
import org.jboss.shrinkwrap.resolver.api.maven.PomEquippedResolveStage;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.runner.RunWith;
import static org.libreccm.core.CoreConstants.*;
import org.apache.commons.codec.binary.Base64;
import org.jboss.arquillian.container.test.api.ShouldThrowException;
import org.libreccm.tests.categories.IntegrationTest;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import static org.junit.Assert.*;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@Category(IntegrationTest.class)
@RunWith(Arquillian.class)
@PersistenceTest
@Transactional(TransactionMode.COMMIT)
public class UserManagerTest {
@Inject
private transient UserRepository userRepository;
@Inject
private transient UserManager userManager;
@PersistenceContext
private transient EntityManager entityManager;
public UserManagerTest() {
}
@BeforeClass
public static void setUpClass() {
}
@AfterClass
public static void tearDownClass() {
}
@Before
public void setUp() {
}
@After
public void tearDown() {
}
@Deployment
public static WebArchive createDeployment() {
final PomEquippedResolveStage pom = Maven
.resolver()
.loadPomFromFile("pom.xml");
final PomEquippedResolveStage dependencies = pom.
importCompileAndRuntimeDependencies();
final File[] libs = dependencies.resolve().withTransitivity().asFile();
for (File lib : libs) {
System.err.printf("Adding file '%s' to test archive...%n",
lib.getName());
}
return ShrinkWrap
.create(WebArchive.class,
"LibreCCM-org.libreccm.core.UserRepositoryTest.war")
.addPackage(User.class.getPackage())
.addPackage(org.libreccm.web.Application.class.getPackage())
.addPackage(org.libreccm.categorization.Category.class.
getPackage())
.addPackage(org.libreccm.l10n.LocalizedString.class.getPackage()).
addPackage(org.libreccm.jpa.EntityManagerProducer.class
.getPackage())
.addPackage(org.libreccm.jpa.utils.MimeTypeConverter.class
.getPackage())
.addPackage(org.libreccm.testutils.EqualsVerifier.class.
getPackage())
.addPackage(org.libreccm.tests.categories.IntegrationTest.class
.getPackage())
.addAsLibraries(libs)
.addAsResource("test-persistence.xml",
"META-INF/persistence.xml")
.addAsWebInfResource("test-web.xml", "WEB-INF/web.xml")
.addAsWebInfResource(EmptyAsset.INSTANCE, "WEB-INF/beans.xml");
}
@Test
@InSequence(10)
public void userRepoIsInjected() {
assertThat(userRepository, is(not(nullValue())));
}
@Test
@InSequence(20)
public void userManagerIsInjected() {
assertThat(userManager, is(not(nullValue())));
}
@Test
@InSequence(30)
public void entityManagerIsInjected() {
assertThat(entityManager, is(not(nullValue())));
}
@Test
@UsingDataSet("datasets/org/libreccm/core/UserRepositoryTest/data.json")
@InSequence(100)
public void updatePassword() throws NoSuchAlgorithmException,
UnsupportedEncodingException {
final User jdoe = userRepository.findById(-10L);
assertThat(jdoe, is(not(nullValue())));
assertThat(jdoe.getScreenName(), is("jdoe"));
final String newPassword = "foobar";
userManager.updatePassword(jdoe, newPassword);
final Base64 base64 = new Base64();
final User user = entityManager.find(User.class, -10L);
final byte[] passwordBytes = newPassword.getBytes(UTF8);
final String salt = user.getSalt();
final byte[] saltBytes = base64.decode(salt);
assertThat(saltBytes.length, is(userManager.getSaltLength()));
final MessageDigest digest = MessageDigest.getInstance(userManager
.getHashAlgorithm());
final byte[] saltedPassword = new byte[passwordBytes.length
+ saltBytes.length];
System.arraycopy(passwordBytes,
0,
saltedPassword,
0,
passwordBytes.length);
System.arraycopy(saltBytes,
0,
saltedPassword,
passwordBytes.length,
saltBytes.length);
final byte[] hashedBytes = digest.digest(saltedPassword);
final String hashed = base64.encodeToString(hashedBytes);
assertThat(user.getPassword(), is(equalTo(hashed)));
}
@Test
@UsingDataSet(
"datasets/org/libreccm/core/UserManagerTest/verify-password.json")
@InSequence(200)
public void verifyPasswordForUser() {
final User user = userRepository.findById(-10L);
//userManager.updatePassword(user, "foobar");
final boolean result = userManager.verifyPasswordForUser(user, "foobar");
assertThat(result, is(true));
}
@Test
@UsingDataSet(
"datasets/org/libreccm/core/UserManagerTest/verify-password.json")
@InSequence(300)
public void verifyPasswordForScreenname() throws UserNotFoundException {
final boolean result = userManager.verifyPasswordForScreenname(
"jdoe", "foobar");
assertThat(result, is(true));
}
@Test
@UsingDataSet(
"datasets/org/libreccm/core/UserManagerTest/verify-password.json")
@InSequence(400)
public void verifyPasswordForEmail() throws UserNotFoundException {
final boolean result = userManager.verifyPasswordForEmail(
"john.doe@example.com", "foobar");
assertThat(result, is(true));
}
@Test
@UsingDataSet(
"datasets/org/libreccm/core/UserManagerTest/verify-password.json")
@InSequence(500)
public void verifyPasswordForUserWrongPassword() {
final User user = userRepository.findById(-10L);
final boolean result = userManager.verifyPasswordForUser(user, "wrong");
assertThat(result, is(false));
}
@Test
@UsingDataSet(
"datasets/org/libreccm/core/UserManagerTest/verify-password.json")
@InSequence(600)
public void verifyPasswordForScreennameWrongPassword() throws
UserNotFoundException {
final boolean result = userManager.verifyPasswordForScreenname(
"jdoe", "wrong");
assertThat(result, is(false));
}
@Test
@UsingDataSet(
"datasets/org/libreccm/core/UserManagerTest/verify-password.json")
@InSequence(400)
public void verifyPasswordForEmailWrongPassword() throws
UserNotFoundException {
final boolean result = userManager.verifyPasswordForEmail(
"john.doe@example.com", "wrong");
assertThat(result, is(false));
}
@Test(expected = UserNotFoundException.class)
@ShouldThrowException(UserNotFoundException.class)
@UsingDataSet(
"datasets/org/libreccm/core/UserManagerTest/verify-password.json")
@InSequence(700)
public void verifyPasswordForScreennameNoUser() throws UserNotFoundException {
userManager.verifyPasswordForScreenname("nobody", "foobar");
}
@Test(expected = UserNotFoundException.class)
@ShouldThrowException(UserNotFoundException.class)
@UsingDataSet(
"datasets/org/libreccm/core/UserManagerTest/verify-password.json")
@InSequence(800)
public void verifyPasswordForEmailNoUser() throws UserNotFoundException {
userManager.verifyPasswordForEmail("nobody@example.com", "foobar");
}
}

View File

@ -0,0 +1,31 @@
{
"subjects":
[
{
"subject_id": -10
}
],
"subject_email_addresses":
[
{
"subject_id": -10,
"email_address": "john.doe@example.com",
"bouncing": false,
"verified": true
}
],
"ccm_users":
[
{
"banned": false,
"hash_algorithm": "SHA-512",
"family_name": "Doe",
"given_name": "John",
"password": "C+o2w6mp+eLrbluMEgKMVSdP50A9BMethXN8R3yihtkbzt7WfWsde2nmq/t5gq6im3J8i3jw4Y3YrKHou8JQ2A==",
"password_reset_required": false,
"salt": "Fu8FPgqAal4GZp1hDjkOB+t6ITRCcO7HBoN5Xqf29UnVj5NUdUFZRTyKYMBEx6JmZGmHcMDG9OGVCKcEM9oyScSRreJs4B51wM44NM6KeRwbCf+VhBn14DkBrl40ygraNf+AJacKpMyCpFI0O/Am7mMDWL4flskBsylkxaQn3vKfzgN5MVG2szW//I6Q6YEH9AuL8LauS6fKaVynMzzu3xzD8Hjqvvlnzym898eom2lqScPfg5g4e8Ww13HCHAYe6twupAW/BjUNax5HSioEisZN/P1UGrde8uFEj+hbbavrWYZuilPuEu25+/98jyXx6542agqrWN8j0SFYcIyOgA==",
"screen_name": "jdoe",
"subject_id": -10
}
]
}