Add token-based authentication (without rejection handling)

This commit is contained in:
Kai S. K. Engelbart 2020-09-19 11:37:42 +02:00
parent 31cb22035b
commit f21d077522
Signed by: kske
GPG Key ID: 8BEB13EC5DF7EF13
10 changed files with 178 additions and 35 deletions

View File

@ -8,6 +8,10 @@ import envoy.data.*;
import envoy.event.*; import envoy.event.*;
import envoy.util.SerializationUtils; import envoy.util.SerializationUtils;
import dev.kske.eventbus.Event;
import dev.kske.eventbus.EventBus;
import dev.kske.eventbus.EventListener;
/** /**
* Stores information about the current {@link User} and their {@link Chat}s. * Stores information about the current {@link User} and their {@link Chat}s.
* For message ID generation a {@link IDGenerator} is stored as well. * For message ID generation a {@link IDGenerator} is stored as well.
@ -21,7 +25,7 @@ import envoy.util.SerializationUtils;
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public final class LocalDB { public final class LocalDB implements EventListener {
private User user; private User user;
private Map<String, User> users = new HashMap<>(); private Map<String, User> users = new HashMap<>();
@ -29,7 +33,8 @@ public final class LocalDB {
private IDGenerator idGenerator; private IDGenerator idGenerator;
private CacheMap cacheMap = new CacheMap(); private CacheMap cacheMap = new CacheMap();
private Instant lastSync = Instant.EPOCH; private Instant lastSync = Instant.EPOCH;
private File dbDir, userFile, idGeneratorFile, usersFile; private String authToken;
private File dbDir, userFile, idGeneratorFile, lastLoginFile, usersFile;
/** /**
* Constructs an empty local database. To serialize any user-specific data to * Constructs an empty local database. To serialize any user-specific data to
@ -42,6 +47,7 @@ public final class LocalDB {
*/ */
public LocalDB(File dbDir) throws IOException { public LocalDB(File dbDir) throws IOException {
this.dbDir = dbDir; this.dbDir = dbDir;
EventBus.getInstance().registerListener(this);
// Test if the database directory is actually a directory // Test if the database directory is actually a directory
if (dbDir.exists() && !dbDir.isDirectory()) if (dbDir.exists() && !dbDir.isDirectory())
@ -49,6 +55,7 @@ public final class LocalDB {
// Initialize global files // Initialize global files
idGeneratorFile = new File(dbDir, "id_gen.db"); idGeneratorFile = new File(dbDir, "id_gen.db");
lastLoginFile = new File(dbDir, "last_login.db");
usersFile = new File(dbDir, "users.db"); usersFile = new File(dbDir, "users.db");
// Initialize offline caches // Initialize offline caches
@ -76,12 +83,16 @@ public final class LocalDB {
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void save(boolean isOnline) throws IOException { public void save(boolean isOnline) throws IOException {
// Save users // Save users
SerializationUtils.write(usersFile, users); SerializationUtils.write(usersFile, users);
// Save user data and last sync time stamp // Save user data and last sync time stamp
if (user != null) SerializationUtils.write(userFile, chats, cacheMap, isOnline ? Instant.now() : lastSync); if (user != null) SerializationUtils.write(userFile, chats, cacheMap, isOnline ? Instant.now() : lastSync);
// Save last login information
if (authToken != null) SerializationUtils.write(lastLoginFile, user, authToken);
// Save id generator // Save id generator
if (hasIDGenerator()) SerializationUtils.write(idGeneratorFile, idGenerator); if (hasIDGenerator()) SerializationUtils.write(idGeneratorFile, idGenerator);
} }
@ -120,6 +131,20 @@ public final class LocalDB {
idGenerator = SerializationUtils.read(idGeneratorFile, IDGenerator.class); idGenerator = SerializationUtils.read(idGeneratorFile, IDGenerator.class);
} catch (ClassNotFoundException | IOException e) {} } catch (ClassNotFoundException | IOException e) {}
} }
/**
* Loads the last login information. Any exception thrown during this process is
* ignored.
*
* @since Envoy Client v0.2-beta
*/
public void loadLastLogin() {
try (var in = new ObjectInputStream(new FileInputStream(lastLoginFile))) {
user = (User) in.readObject();
authToken = (String) in.readObject();
} catch (ClassNotFoundException | IOException e) {}
}
/** /**
* Synchronizes the contact list of the client user with the chat and user * Synchronizes the contact list of the client user with the chat and user
* storage. * storage.
@ -143,6 +168,11 @@ public final class LocalDB {
.forEach(chats::add); .forEach(chats::add);
} }
@Event
private void onNewAuthToken(NewAuthToken evt) {
authToken = evt.get();
}
/** /**
* @return a {@code Map<String, User>} of all users stored locally with their * @return a {@code Map<String, User>} of all users stored locally with their
* user names as keys * user names as keys
@ -204,6 +234,12 @@ public final class LocalDB {
*/ */
public Instant getLastSync() { return lastSync; } public Instant getLastSync() { return lastSync; }
/**
* @return the authentication token of the user
* @since Envoy Client v0.2-beta
*/
public String getAuthToken() { return authToken; }
/** /**
* Searches for a message by ID. * Searches for a message by ID.
* *

View File

@ -77,10 +77,12 @@ public final class Client implements EventListener, Closeable {
// Create object receiver // Create object receiver
receiver = new Receiver(socket.getInputStream()); receiver = new Receiver(socket.getInputStream());
// Register user creation processor, contact list processor and message cache // Register user creation processor, contact list processor, message cache and
// authentication token
receiver.registerProcessor(User.class, sender -> this.sender = sender); receiver.registerProcessor(User.class, sender -> this.sender = sender);
receiver.registerProcessors(cacheMap.getMap()); receiver.registerProcessors(cacheMap.getMap());
receiver.registerProcessor(HandshakeRejection.class, evt -> { rejected = true; eventBus.dispatch(evt); }); receiver.registerProcessor(HandshakeRejection.class, evt -> { rejected = true; eventBus.dispatch(evt); });
receiver.registerProcessor(NewAuthToken.class, eventBus::dispatch);
rejected = false; rejected = false;
@ -173,8 +175,7 @@ public final class Client implements EventListener, Closeable {
receiver.registerProcessor(ProfilePicChange.class, eventBus::dispatch); receiver.registerProcessor(ProfilePicChange.class, eventBus::dispatch);
// Process requests to not send any more attachments as they will not be shown // Process requests to not send any more attachments as they will not be shown
// to // to other users
// other users
receiver.registerProcessor(NoAttachments.class, eventBus::dispatch); receiver.registerProcessor(NoAttachments.class, eventBus::dispatch);
// Process group creation results - they might have been disabled on the server // Process group creation results - they might have been disabled on the server

View File

@ -77,6 +77,7 @@ public final class Startup extends Application {
// Prepare handshake // Prepare handshake
localDB.loadIDGenerator(); localDB.loadIDGenerator();
localDB.loadLastLogin();
context.setLocalDB(localDB); context.setLocalDB(localDB);
// Configure stage // Configure stage
@ -87,9 +88,19 @@ public final class Startup extends Application {
final var sceneContext = new SceneContext(stage); final var sceneContext = new SceneContext(stage);
context.setSceneContext(sceneContext); context.setSceneContext(sceneContext);
// Authenticate with token if present
if (localDB.getAuthToken() != null) {
logger.info("Attempting authentication with token...");
localDB.initializeUserStorage();
localDB.loadUserData();
performHandshake(LoginCredentials.loginWithToken(localDB.getUser().getName(), localDB.getAuthToken(), VERSION, localDB.getLastSync()));
// TODO: handle unsuccessful handshake
} else {
// Load login scene // Load login scene
sceneContext.load(SceneInfo.LOGIN_SCENE); sceneContext.load(SceneInfo.LOGIN_SCENE);
} }
}
/** /**
* Tries to perform a Handshake with the server. * Tries to perform a Handshake with the server.

View File

@ -23,4 +23,5 @@ module envoy.client {
opens envoy.client.ui.custom to javafx.graphics, javafx.fxml; opens envoy.client.ui.custom to javafx.graphics, javafx.fxml;
opens envoy.client.ui.settings to envoy.client.util; opens envoy.client.ui.settings to envoy.client.util;
opens envoy.client.net to dev.kske.eventbus; opens envoy.client.net to dev.kske.eventbus;
opens envoy.client.data to dev.kske.eventbus;
} }

View File

@ -35,6 +35,13 @@ public final class HandshakeRejection extends Event<String> {
*/ */
public static final String WRONG_VERSION = "Incompatible client version"; public static final String WRONG_VERSION = "Incompatible client version";
/**
* Select this value if the client provided an invalid authentication token.
*
* @since Envoy Common v0.2-beta
*/
public static final String INVALID_TOKEN = "Invalid authentication token";
/** /**
* Select this value if the handshake could not be completed for some different * Select this value if the handshake could not be completed for some different
* reason. * reason.

View File

@ -0,0 +1,26 @@
package envoy.event;
/**
* This event can be used to transmit a new authentication token to a client.
* <p>
* Project: <strong>envoy-common</strong><br>
* File: <strong>NewAuthToken.java</strong><br>
* Created: <strong>19.09.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Common v0.2-beta
*/
public class NewAuthToken extends Event<String> {
private static final long serialVersionUID = 0L;
/**
* Constructs a new authentication token event.
*
* @param token the token to transmit
* @since Envoy Common v0.2-beta
*/
public NewAuthToken(String token) {
super(token);
}
}

View File

@ -35,6 +35,8 @@ public final class ServerConfig extends Config {
put("enableIssueReporting", "e-ir", Boolean::parseBoolean); put("enableIssueReporting", "e-ir", Boolean::parseBoolean);
put("enableGroups", "e-g", Boolean::parseBoolean); put("enableGroups", "e-g", Boolean::parseBoolean);
put("enableAttachments", "e-a", Boolean::parseBoolean); put("enableAttachments", "e-a", Boolean::parseBoolean);
// user authentication
put("authTokenExpiration", "tok-exp", Integer::parseInt);
} }
/** /**
@ -93,4 +95,10 @@ public final class ServerConfig extends Config {
* @since Envoy Server v0.2-beta * @since Envoy Server v0.2-beta
*/ */
public String getIssueAuthToken() { return (String) items.get("issueAuthToken").get(); } public String getIssueAuthToken() { return (String) items.get("issueAuthToken").get(); }
/**
* @return the amount of days after which user authentication tokens expire
* @since Envoy Server v0.2-beta
*/
public Integer getAuthTokenExpiration() { return (Integer) items.get("authTokenExpiration").get(); }
} }

View File

@ -5,26 +5,18 @@ import static envoy.data.User.UserStatus.ONLINE;
import static envoy.event.HandshakeRejection.*; import static envoy.event.HandshakeRejection.*;
import java.time.Instant; import java.time.Instant;
import java.util.Collections; import java.time.temporal.ChronoUnit;
import java.util.HashSet; import java.util.*;
import java.util.List;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.persistence.NoResultException; import javax.persistence.NoResultException;
import envoy.data.LoginCredentials; import envoy.data.LoginCredentials;
import envoy.event.GroupMessageStatusChange; import envoy.event.*;
import envoy.event.HandshakeRejection; import envoy.server.data.*;
import envoy.event.MessageStatusChange; import envoy.server.net.*;
import envoy.server.data.GroupMessage; import envoy.server.util.*;
import envoy.server.data.PersistenceManager; import envoy.util.*;
import envoy.server.data.User;
import envoy.server.net.ConnectionManager;
import envoy.server.net.ObjectWriteProxy;
import envoy.server.util.PasswordUtil;
import envoy.server.util.VersionUtil;
import envoy.util.Bounds;
import envoy.util.EnvoyLog;
/** /**
* This {@link ObjectProcessor} handles {@link LoginCredentials}.<br> * This {@link ObjectProcessor} handles {@link LoginCredentials}.<br>
@ -62,24 +54,39 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
try { try {
user = persistenceManager.getUserByName(credentials.getIdentifier()); user = persistenceManager.getUserByName(credentials.getIdentifier());
// Checking if user is already online // Check if the user is already online
if (connectionManager.isOnline(user.getID())) { if (connectionManager.isOnline(user.getID())) {
logger.warning(user + " is already online!"); logger.warning(user + " is already online!");
writeProxy.write(socketID, new HandshakeRejection(INTERNAL_ERROR)); writeProxy.write(socketID, new HandshakeRejection(INTERNAL_ERROR));
return; return;
} }
// Evaluating the correctness of the password hash
// Authenticate with password or token
if (credentials.usesToken()) {
// Check the token
if (user.getAuthToken() == null || user.getAuthTokenExpiration().isBefore(Instant.now())
|| !user.getAuthToken().equals(credentials.getPassword())) {
logger.info(user + " tried to use an invalid token.");
writeProxy.write(socketID, new HandshakeRejection(INVALID_TOKEN));
return;
}
} else {
// Check the password hash
if (!PasswordUtil.validate(credentials.getPassword(), user.getPasswordHash())) { if (!PasswordUtil.validate(credentials.getPassword(), user.getPasswordHash())) {
logger.info(user + " has entered the wrong password."); logger.info(user + " has entered the wrong password.");
writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER)); writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER));
return; return;
} }
}
} catch (NoResultException e) { } catch (NoResultException e) {
logger.info("The requested user does not exist."); logger.info("The requested user does not exist.");
writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER)); writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER));
return; return;
} }
} else { } else {
// Validate user name // Validate user name
if (!Bounds.isValidContactName(credentials.getIdentifier())) { if (!Bounds.isValidContactName(credentials.getIdentifier())) {
logger.info("The requested user name is not valid."); logger.info("The requested user name is not valid.");
@ -87,7 +94,8 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
return; return;
} }
try { try {
// Checking that no user already has this identifier
// Check if the name is taken
PersistenceManager.getInstance().getUserByName(credentials.getIdentifier()); PersistenceManager.getInstance().getUserByName(credentials.getIdentifier());
// This code only gets executed if this user already exists // This code only gets executed if this user already exists
@ -114,6 +122,15 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
user.setStatus(ONLINE); user.setStatus(ONLINE);
UserStatusChangeProcessor.updateUserStatus(user); UserStatusChangeProcessor.updateUserStatus(user);
// Generate a new token if requested
if (credentials.requestToken()) {
String token = AuthTokenGenerator.nextToken();
user.setAuthToken(token);
user.setAuthTokenExpiration(Instant.now().plus(ServerConfig.getInstance().getAuthTokenExpiration().longValue(), ChronoUnit.DAYS));
persistenceManager.updateContact(user);
writeProxy.write(socketID, new NewAuthToken(token));
}
final var pendingMessages = PersistenceManager.getInstance().getPendingMessages(user, credentials.getLastSync()); final var pendingMessages = PersistenceManager.getInstance().getPendingMessages(user, credentials.getLastSync());
pendingMessages.removeIf(GroupMessage.class::isInstance); pendingMessages.removeIf(GroupMessage.class::isInstance);
logger.fine("Sending " + pendingMessages.size() + " pending messages to " + user + "..."); logger.fine("Sending " + pendingMessages.size() + " pending messages to " + user + "...");

View File

@ -0,0 +1,35 @@
package envoy.server.util;
import java.security.SecureRandom;
/**
* Provides a secure token generation algorithm.
* <p>
* Project: <strong>envoy-server</strong><br>
* File: <strong>AuthTokenGenerator.java</strong><br>
* Created: <strong>19.09.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Server v0.2-beta
*/
public final class AuthTokenGenerator {
private static final int TOKEN_LENGTH = 128;
private static final char[] CHARACTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray();
private static final char[] BUFF = new char[TOKEN_LENGTH];
private static final SecureRandom RANDOM = new SecureRandom();
private AuthTokenGenerator() {}
/**
* Generates a random authentication token.
*
* @return a random authentication token
* @since Envoy Server v0.2-beta
*/
public static String nextToken() {
for (int i = 0; i < BUFF.length; ++i)
BUFF[i] = CHARACTERS[RANDOM.nextInt(CHARACTERS.length)];
return new String(BUFF);
}
}

View File

@ -10,3 +10,4 @@ featureLabel=119
issueAuthToken= issueAuthToken=
consoleLevelBarrier=FINEST consoleLevelBarrier=FINEST
fileLevelBarrier=WARNING fileLevelBarrier=WARNING
authTokenExpiration=90