package hu.sch.ejb;
import hu.sch.domain.enums.SvieMembershipType;
import hu.sch.domain.enums.SvieStatus;
import hu.sch.domain.user.Gender;
import hu.sch.domain.user.LostPasswordToken;
import hu.sch.domain.user.User;
import hu.sch.domain.user.UserStatus;
import static hu.sch.ejb.MailManagerBean.getMailString;
import hu.sch.services.AccountManager;
import hu.sch.services.Roles;
import hu.sch.services.SystemManagerLocal;
import hu.sch.services.UserManagerLocal;
import hu.sch.util.config.Configuration;
import hu.sch.services.exceptions.DuplicatedUserException;
import hu.sch.services.exceptions.PekErrorCode;
import hu.sch.services.exceptions.PekException;
import hu.sch.util.hash.Hashing;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Date;
import java.util.Random;
import javax.annotation.Resource;
import javax.ejb.SessionContext;
import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.NonUniqueResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceException;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.time.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author balo
*/
@Stateless
public class AccountManagerBean implements AccountManager {
private static Logger logger = LoggerFactory.getLogger(AccountManagerBean.class);
//
private static final int PASSWORD_SALT_LENGTH = 8;
public static final long LOST_PW_TOKEN_VALID_MS = 24 * 60 * 60 * 1000; //24 hours in ms
//
@Inject
private Configuration config;
//
@PersistenceContext
private EntityManager em;
//
@Inject
private UserManagerLocal userManager;
@Inject
private SystemManagerLocal systemManager;
@Inject
private MailManagerBean mailManager;
@Resource
private SessionContext sessionContext;
public AccountManagerBean() {
}
public AccountManagerBean(final EntityManager em) {
this.em = em;
}
/**
* {@inheritDoc}
*/
@Override
public void createUser(User user, String password) {
final byte[] salt = generateSalt();
final String passwordDigest = hashPassword(password, salt);
final boolean isAdmin = sessionContext.isCallerInRole(Roles.ADMIN);
if (!isAdmin) {
user.setSalt(Base64.encodeBase64String(salt));
user.setPasswordDigest(passwordDigest);
}
user.setSvieMembershipType(SvieMembershipType.NEMTAG);
user.setSvieStatus(SvieStatus.NEMTAG);
user.setGender(Gender.NOTSPECIFIED);
user.setConfirmationCode(generateConfirmationCode());
sendConfirmationEmail(user, isAdmin);
em.persist(user);
}
/**
* Generates and sets a random confirmation code for the user.
*/
private String generateConfirmationCode() {
final Random rnd = new SecureRandom();
final byte[] bytes = new byte[48];
String confirm = null;
final TypedQuery<Long> q = em.createQuery("SELECT COUNT(u) FROM User u WHERE u.confirmationCode = :confirm", Long.class);
// check for uniqueness!
do {
rnd.nextBytes(bytes);
confirm = Base64.encodeBase64URLSafeString(bytes);
q.setParameter("confirm", confirm);
} while (!q.getSingleResult().equals(0L));
// 48 byte of randomness encoded into 64 characters
return confirm;
}
/**
* {@inheritDoc}
*/
@Override
public void confirm(final User user, final String password) {
if (password != null) {
byte[] salt = generateSalt();
String passwordDigest = hashPassword(password, salt);
user.setSalt(Base64.encodeBase64String(salt));
user.setPasswordDigest(passwordDigest);
}
user.setConfirmationCode(null);
user.setUserStatus(UserStatus.ACTIVE);
em.merge(user);
}
/**
* Sends an email to the user with the confirmation code.
*
* @param user
* @param isCreatedByAdmin the email contains different texts depends from
* this (selfreg vs. admin reg)
* @return
*/
private boolean sendConfirmationEmail(User user, boolean isCreatedByAdmin) {
String subject, body;
subject = getMailString(MailManagerBean.MAIL_CONFIRMATION_SUBJECT);
if (isCreatedByAdmin) {
body = String.format(
getMailString(MailManagerBean.MAIL_CONFIRMATION_ADMIN_BODY),
user.getFullName(),
generateConfirmationLink(user));
} else {
body = String.format(
getMailString(MailManagerBean.MAIL_CONFIRMATION_BODY),
user.getFullName(),
generateConfirmationLink(user));
}
return mailManager.sendEmail(user.getEmailAddress(), subject, body);
}
private String generateConfirmationLink(final User user) {
// TODO: github/#106: fix double domain
// TODO: fix url
return String.format("https://%s/profile/confirm/code/%s",
config.getDomain(),
user.getConfirmationCode());
}
/**
* {@inheritDoc}
*/
@Override
public void changePassword(String screenName, String oldPwd, String newPwd) {
User user = userManager.findUserByScreenName(screenName);
byte[] salt = Base64.decodeBase64(user.getSalt());
String passwordHash = hashPassword(oldPwd, salt);
if (!passwordHash.equals(user.getPasswordDigest())) {
logger.info("Password change requested with invalid password for user {}", user.getId());
throw new PekException(PekErrorCode.INVALID_PASSWORD, "Invalid original password.");
}
user.setPasswordDigest(hashPassword(newPwd, salt));
em.merge(user);
}
private String hashPassword(String password, byte[] salt) {
byte[] passwordBytes;
passwordBytes = password.getBytes(StandardCharsets.UTF_8);
byte[] hashInput = new byte[passwordBytes.length + salt.length];
System.arraycopy(passwordBytes, 0, hashInput, 0, passwordBytes.length);
System.arraycopy(salt, 0, hashInput, passwordBytes.length, salt.length);
return Hashing.sha1(hashInput).toBase64();
}
private byte[] generateSalt() {
byte[] salt = new byte[PASSWORD_SALT_LENGTH];
new SecureRandom().nextBytes(salt);
return salt;
}
/**
* {@inheritDoc}
*/
@Override
public boolean sendUserNameReminder(final String email) {
if (email == null || email.isEmpty()) {
throw new IllegalArgumentException("email argument can't be null when sending user name reminder");
}
try {
final User result = userManager.findUserByEmail(email);
if (result == null) {
throw new PekException(PekErrorCode.ENTITY_NOT_FOUND, String.format("User for %s email was not found.", email));
} else {
final String subject = MailManagerBean.getMailString(MailManagerBean.MAIL_USERNAME_REMINDER_SUBJECT);
final String messageBody;
if (systemManager.getNewbieTime()) {
messageBody = String.format(
MailManagerBean.getMailString(MailManagerBean.MAIL_USERNAME_REMINDER_BODY_NEWBIE),
result.getFirstName(), result.getScreenName());
} else {
messageBody = String.format(
MailManagerBean.getMailString(MailManagerBean.MAIL_USERNAME_REMINDER_BODY),
result.getFirstName(), result.getScreenName());
}
return mailManager.sendEmail(email, subject, messageBody);
}
} catch (DuplicatedUserException ex) {
logger.error("sendUserNameReminder: Duplicated user with email={}", email);
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public boolean sendLostPasswordChangeLink(final String email) {
if (email == null || email.isEmpty()) {
throw new IllegalArgumentException("email argument can't be null when sending password change link");
}
try {
final User user = userManager.findUserByEmail(email);
if (user == null) {
throw new PekException(PekErrorCode.ENTITY_NOT_FOUND, String.format("User for %s email was not found.", email));
}
final String subject = MailManagerBean.getMailString(MailManagerBean.MAIL_LOST_PASSWORD_SUBJECT);
final String body;
if (systemManager.getNewbieTime()) {
body = MailManagerBean.getMailString(MailManagerBean.MAIL_LOST_PASSWORD_BODY_NEWBIE);
} else {
body = MailManagerBean.getMailString(MailManagerBean.MAIL_LOST_PASSWORD_BODY);
}
String name = user.getFirstName();
if (systemManager.getNewbieTime()) {
name = user.getFullName();
}
logger.debug("sendLostPasswordChangeLink, user found={}", user.toString());
final LostPasswordToken token = getTokenByUser(user);
final String message = String.format(body,
name, user.getScreenName(), generateLostPasswordLink(token));
return mailManager.sendEmail(email, subject, message);
} catch (DuplicatedUserException ex) {
logger.error("sendLostPasswordChangeLink: Duplicated user with email={}", email);
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public void replaceLostPassword(final String tokenKey, final String password)
{
//checks the token again (validity, expiry, etc)
final User user = getUserByLostPasswordToken(tokenKey);
logger.info("Replace lost password for user={}", user.getScreenName());
byte[] salt = generateSalt();
String passwordDigest = hashPassword(password, salt);
user.setSalt(Base64.encodeBase64String(salt));
user.setPasswordDigest(passwordDigest);
//removes the used token
em.remove(em.find(LostPasswordToken.class, user.getId()));
em.merge(user);
}
/**
* {@inheritDoc}
*/
@Override
public User getUserByLostPasswordToken(final String tokenKey)
{
final TypedQuery<LostPasswordToken> q
= em.createNamedQuery(LostPasswordToken.getByToken, LostPasswordToken.class);
q.setParameter("token", tokenKey);
try {
final LostPasswordToken token = q.getSingleResult();
final long currentTimeMillis = System.currentTimeMillis();
if (currentTimeMillis > token.getCreated().getTime() + LOST_PW_TOKEN_VALID_MS) {
logger.info("Somebody tried to use an expired token={}", tokenKey);
throw new PekException(PekErrorCode.TOKEN_EXPIRED, "Password reset token is expired.");
}
return token.getSubjectUser();
} catch (NoResultException | NonUniqueResultException ex) {
logger.info("Somebody tried to use an invalid token={}", tokenKey);
throw new PekException(PekErrorCode.TOKEN_NOT_FOUND, "Token was not found.");
}
}
/**
* {@inheritDoc}
*/
@Override
public void removeExpiredLostPasswordTokens() {
final Date deleteBefore = DateUtils.addMilliseconds(new Date(),
(int) -LOST_PW_TOKEN_VALID_MS);
final Query cleanUpQuery = em.createNamedQuery(LostPasswordToken.removeExpired);
cleanUpQuery.setParameter("time_in_past", deleteBefore);
final int deleteCount = cleanUpQuery.executeUpdate();
logger.info("deleted lostpw tokens={}", deleteCount);
}
private String generateLostPasswordLink(final LostPasswordToken token) {
// TODO: github/#106: fix double domain
// TODO: fix url
return String.format("https://%s/profile/replacelostpassword/token/%s",
config.getDomain(),
token.getToken());
}
private LostPasswordToken getTokenByUser(final User user) {
//remove existing token if it exists because LostPasswordToken is immutable
final LostPasswordToken existingToken = em.find(LostPasswordToken.class, user.getId());
if (existingToken != null) {
em.remove(existingToken);
}
//create new token
final Random rnd = new SecureRandom();
final byte[] bytes = new byte[48];
rnd.nextBytes(bytes);
final String newTokenKey = Base64.encodeBase64URLSafeString(bytes);
logger.debug("create new lostpw token with code={}, length={}",
newTokenKey, newTokenKey.length());
final LostPasswordToken newToken = new LostPasswordToken(user, newTokenKey, new Date());
em.persist(newToken);
em.flush();
return newToken;
}
}