Implement a password reset flow.

This commit is contained in:
Me Car 2016-01-10 20:00:08 +09:00
parent 23d47be04c
commit 746d034461
14 changed files with 900 additions and 37 deletions

View file

@ -14,7 +14,7 @@ import org.apache.shiro.crypto.hash.Hash;
@DatabaseTable(tableName = "authorized_user")
public class AuthorizedUser {
@DatabaseField(indexName = "name_index")
@DatabaseField(indexName = "name_index", unique = true)
protected String name;
@DatabaseField
@ -29,7 +29,7 @@ public class AuthorizedUser {
@DatabaseField
protected int hashIterations;
@DatabaseField
@DatabaseField(indexName = "email_index", unique = true)
protected String email;
public AuthorizedUser() {
@ -53,4 +53,8 @@ public class AuthorizedUser {
ByteSource.Util.bytes(Base64.decode(this.salt)), "");
return matcher.doCredentialsMatch(token, info);
}
public String getName() {
return this.name;
}
}

View file

@ -3,6 +3,7 @@ package mage.server;
import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.dao.DaoManager;
import com.j256.ormlite.jdbc.JdbcConnectionSource;
import com.j256.ormlite.stmt.DeleteBuilder;
import com.j256.ormlite.stmt.QueryBuilder;
import com.j256.ormlite.stmt.SelectArg;
import com.j256.ormlite.support.ConnectionSource;
@ -11,7 +12,6 @@ import com.j256.ormlite.table.TableUtils;
import java.io.File;
import java.sql.SQLException;
import java.util.List;
import java.util.concurrent.Callable;
import mage.cards.repository.RepositoryUtil;
import org.apache.log4j.Logger;
import org.apache.shiro.crypto.RandomNumberGenerator;
@ -54,25 +54,25 @@ public enum AuthorizedUserRepository {
public void add(final String userName, final String password, final String email) {
try {
dao.callBatchTasks(new Callable<Object>() {
@Override
public Object call() throws Exception {
try {
Hash hash = new SimpleHash(Sha256Hash.ALGORITHM_NAME, password, rng.nextBytes(), 1024);
AuthorizedUser user = new AuthorizedUser(userName, hash, email);
dao.create(user);
} catch (SQLException ex) {
Logger.getLogger(AuthorizedUserRepository.class).error("Error adding a user to DB - ", ex);
}
return null;
}
});
} catch (Exception ex) {
Logger.getLogger(AuthorizedUserRepository.class).error("Error adding a authorized_user - ", ex);
Hash hash = new SimpleHash(Sha256Hash.ALGORITHM_NAME, password, rng.nextBytes(), 1024);
AuthorizedUser user = new AuthorizedUser(userName, hash, email);
dao.create(user);
} catch (SQLException ex) {
Logger.getLogger(AuthorizedUserRepository.class).error("Error adding a user to DB - ", ex);
}
}
public AuthorizedUser get(String userName) {
public void remove(final String userName) {
try {
DeleteBuilder<AuthorizedUser, Object> db = dao.deleteBuilder();
db.where().eq("name", new SelectArg(userName));
db.delete();
} catch (SQLException ex) {
Logger.getLogger(AuthorizedUserRepository.class).error("Error removing a user from DB - ", ex);
}
}
public AuthorizedUser getByName(String userName) {
try {
QueryBuilder<AuthorizedUser, Object> qb = dao.queryBuilder();
qb.where().eq("name", new SelectArg(userName));
@ -87,6 +87,21 @@ public enum AuthorizedUserRepository {
return null;
}
public AuthorizedUser getByEmail(String userName) {
try {
QueryBuilder<AuthorizedUser, Object> qb = dao.queryBuilder();
qb.where().eq("email", new SelectArg(userName));
List<AuthorizedUser> results = dao.query(qb.prepare());
if (results.size() == 1) {
return results.get(0);
}
return null;
} catch (SQLException ex) {
Logger.getLogger(AuthorizedUserRepository.class).error("Error getting a authorized_user - ", ex);
}
return null;
}
public void closeDB() {
try {
if (dao != null && dao.getConnectionSource() != null) {

View file

@ -27,9 +27,12 @@
*/
package mage.server;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
@ -95,9 +98,17 @@ public class MageServerImpl implements MageServer {
private static final Logger logger = Logger.getLogger(MageServerImpl.class);
private static final ExecutorService callExecutor = ThreadExecutor.getInstance().getCallExecutor();
private static final SecureRandom RANDOM = new SecureRandom();
private final String adminPassword;
private final boolean testMode;
private final LinkedHashMap<String, String> activeAuthTokens = new LinkedHashMap<String, String>() {
@Override
protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
// Keep the latest 1024 auth tokens in memory.
return size() > 1024;
}
};
public MageServerImpl(String adminPassword, boolean testMode) {
this.adminPassword = adminPassword;
@ -110,6 +121,50 @@ public class MageServerImpl implements MageServer {
return SessionManager.getInstance().registerUser(sessionId, userName, password, email);
}
// generateAuthToken returns a uniformly distributed 6-digits string.
static private String generateAuthToken() {
return String.format("%06d", RANDOM.nextInt(1000000));
}
@Override
public boolean emailAuthToken(String sessionId, String email) throws MageException {
AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.getByEmail(email);
if (authorizedUser == null) {
sendErrorMessageToClient(sessionId, "No user was found with the email address " + email);
logger.info("Auth token is requested for " + email + " but there's no such user in DB");
return false;
}
String authToken = generateAuthToken();
activeAuthTokens.put(email, authToken);
if (!GmailClient.sendMessage(email, "XMage Password Reset Auth Token",
"Use this auth token to reset your password: " + authToken + "\n" +
"It's valid until the next server restart.")) {
sendErrorMessageToClient(sessionId, "There was an error inside the server while emailing an auth token");
return false;
}
return true;
}
@Override
public boolean resetPassword(String sessionId, String email, String authToken, String password) throws MageException {
String storedAuthToken = activeAuthTokens.get(email);
if (storedAuthToken == null || !storedAuthToken.equals(authToken)) {
sendErrorMessageToClient(sessionId, "Invalid auth token");
logger.info("Invalid auth token " + authToken + " is sent for " + email);
return false;
}
AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.getByEmail(email);
if (authorizedUser == null) {
sendErrorMessageToClient(sessionId, "The user is no longer in the DB");
logger.info("Auth token is valid, but the user with email address " + email + " is no longer in the DB");
return false;
}
AuthorizedUserRepository.instance.remove(authorizedUser.getName());
AuthorizedUserRepository.instance.add(authorizedUser.getName(), password, email);
activeAuthTokens.remove(email);
return true;
}
@Override
public boolean registerClient(String userName, String sessionId, MageVersion version) throws MageException {
// This method is deprecated, so just inform the server version.
@ -1045,6 +1100,15 @@ public class MageServerImpl implements MageServer {
}
}
private void sendErrorMessageToClient(final String sessionId, final String message) throws MageException {
execute("sendErrorMessageToClient", sessionId, new Action() {
@Override
public void execute() {
SessionManager.getInstance().sendErrorMessageToClient(sessionId, message);
}
});
}
protected void execute(final String actionName, final String sessionId, final Action action, boolean checkAdminRights) throws MageException {
if (checkAdminRights) {
if (!SessionManager.getInstance().isAdmin(sessionId)) {

View file

@ -95,6 +95,11 @@ public class Session {
sendErrorMessageToClient(returnMessage);
return returnMessage;
}
returnMessage = validateEmail(email);
if (returnMessage != null) {
sendErrorMessageToClient(returnMessage);
return returnMessage;
}
AuthorizedUserRepository.instance.add(userName, password, email);
if (GmailClient.sendMessage(email, "XMage Registration Completed",
"You are successfully registered as " + userName + ".")) {
@ -121,7 +126,7 @@ public class Session {
if (m.find()) {
return "User name '" + userName + "' includes not allowed characters: use a-z, A-Z and 0-9";
}
AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.get(userName);
AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.getByName(userName);
if (authorizedUser != null) {
return "User name '" + userName + "' already in use";
}
@ -147,6 +152,14 @@ public class Session {
return null;
}
static private String validateEmail(String email) {
AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.getByEmail(email);
if (authorizedUser != null) {
return "Email address '" + email + "' is associated with another user";
}
return null;
}
public String connectUser(String userName, String password) throws MageException {
String returnMessage = connectUserHandling(userName, password);
if (returnMessage != null) {
@ -161,9 +174,8 @@ public class Session {
public String connectUserHandling(String userName, String password) throws MageException {
this.isAdmin = false;
if (ConfigSettings.getInstance().isAuthenticationActivated()) {
AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.get(userName);
AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.getByName(userName);
if (authorizedUser == null || !authorizedUser.doCredentialsMatch(userName, password)) {
return "Wrong username or password";
}
@ -347,7 +359,7 @@ public class Session {
this.host = hostAddress;
}
void sendErrorMessageToClient(String message) {
public void sendErrorMessageToClient(String message) {
List<String> messageData = new LinkedList<>();
messageData.add("Error while connecting to server");
messageData.add(message);

View file

@ -237,4 +237,13 @@ public class SessionManager {
}
return false;
}
public void sendErrorMessageToClient(String sessionId, String message) {
Session session = sessions.get(sessionId);
if (session == null) {
logger.error("Following error message is not delivered because session " + sessionId + " is not found: " + message);
return;
}
session.sendErrorMessageToClient(message);
}
}