Add token-based authentication (without rejection handling)
This commit is contained in:
		@@ -8,6 +8,10 @@ import envoy.data.*;
 | 
			
		||||
import envoy.event.*;
 | 
			
		||||
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.
 | 
			
		||||
 * For message ID generation a {@link IDGenerator} is stored as well.
 | 
			
		||||
@@ -21,7 +25,7 @@ import envoy.util.SerializationUtils;
 | 
			
		||||
 * @author Kai S. K. Engelbart
 | 
			
		||||
 * @since Envoy Client v0.3-alpha
 | 
			
		||||
 */
 | 
			
		||||
public final class LocalDB {
 | 
			
		||||
public final class LocalDB implements EventListener {
 | 
			
		||||
 | 
			
		||||
	private User				user;
 | 
			
		||||
	private Map<String, User>	users		= new HashMap<>();
 | 
			
		||||
@@ -29,7 +33,8 @@ public final class LocalDB {
 | 
			
		||||
	private IDGenerator			idGenerator;
 | 
			
		||||
	private CacheMap			cacheMap	= new CacheMap();
 | 
			
		||||
	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
 | 
			
		||||
@@ -42,6 +47,7 @@ public final class LocalDB {
 | 
			
		||||
	 */
 | 
			
		||||
	public LocalDB(File dbDir) throws IOException {
 | 
			
		||||
		this.dbDir = dbDir;
 | 
			
		||||
		EventBus.getInstance().registerListener(this);
 | 
			
		||||
 | 
			
		||||
		// Test if the database directory is actually a directory
 | 
			
		||||
		if (dbDir.exists() && !dbDir.isDirectory())
 | 
			
		||||
@@ -49,6 +55,7 @@ public final class LocalDB {
 | 
			
		||||
 | 
			
		||||
		// Initialize global files
 | 
			
		||||
		idGeneratorFile	= new File(dbDir, "id_gen.db");
 | 
			
		||||
		lastLoginFile	= new File(dbDir, "last_login.db");
 | 
			
		||||
		usersFile		= new File(dbDir, "users.db");
 | 
			
		||||
 | 
			
		||||
		// Initialize offline caches
 | 
			
		||||
@@ -76,12 +83,16 @@ public final class LocalDB {
 | 
			
		||||
	 * @since Envoy Client v0.3-alpha
 | 
			
		||||
	 */
 | 
			
		||||
	public void save(boolean isOnline) throws IOException {
 | 
			
		||||
 | 
			
		||||
		// Save users
 | 
			
		||||
		SerializationUtils.write(usersFile, users);
 | 
			
		||||
 | 
			
		||||
		// Save user data and last sync time stamp
 | 
			
		||||
		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
 | 
			
		||||
		if (hasIDGenerator()) SerializationUtils.write(idGeneratorFile, idGenerator);
 | 
			
		||||
	}
 | 
			
		||||
@@ -120,6 +131,20 @@ public final class LocalDB {
 | 
			
		||||
			idGenerator = SerializationUtils.read(idGeneratorFile, IDGenerator.class);
 | 
			
		||||
		} 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
 | 
			
		||||
	 * storage.
 | 
			
		||||
@@ -143,6 +168,11 @@ public final class LocalDB {
 | 
			
		||||
			.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
 | 
			
		||||
	 *         user names as keys
 | 
			
		||||
@@ -204,6 +234,12 @@ public final class LocalDB {
 | 
			
		||||
	 */
 | 
			
		||||
	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.
 | 
			
		||||
	 *
 | 
			
		||||
 
 | 
			
		||||
@@ -77,10 +77,12 @@ public final class Client implements EventListener, Closeable {
 | 
			
		||||
		// Create object receiver
 | 
			
		||||
		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.registerProcessors(cacheMap.getMap());
 | 
			
		||||
		receiver.registerProcessor(HandshakeRejection.class, evt -> { rejected = true; eventBus.dispatch(evt); });
 | 
			
		||||
		receiver.registerProcessor(NewAuthToken.class, eventBus::dispatch);
 | 
			
		||||
 | 
			
		||||
		rejected = false;
 | 
			
		||||
 | 
			
		||||
@@ -173,8 +175,7 @@ public final class Client implements EventListener, Closeable {
 | 
			
		||||
		receiver.registerProcessor(ProfilePicChange.class, eventBus::dispatch);
 | 
			
		||||
 | 
			
		||||
		// Process requests to not send any more attachments as they will not be shown
 | 
			
		||||
		// to
 | 
			
		||||
		// other users
 | 
			
		||||
		// to other users
 | 
			
		||||
		receiver.registerProcessor(NoAttachments.class, eventBus::dispatch);
 | 
			
		||||
 | 
			
		||||
		// Process group creation results - they might have been disabled on the server
 | 
			
		||||
 
 | 
			
		||||
@@ -77,6 +77,7 @@ public final class Startup extends Application {
 | 
			
		||||
 | 
			
		||||
		// Prepare handshake
 | 
			
		||||
		localDB.loadIDGenerator();
 | 
			
		||||
		localDB.loadLastLogin();
 | 
			
		||||
		context.setLocalDB(localDB);
 | 
			
		||||
 | 
			
		||||
		// Configure stage
 | 
			
		||||
@@ -87,9 +88,19 @@ public final class Startup extends Application {
 | 
			
		||||
		final var sceneContext = new SceneContext(stage);
 | 
			
		||||
		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
 | 
			
		||||
			sceneContext.load(SceneInfo.LOGIN_SCENE);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Tries to perform a Handshake with the server.
 | 
			
		||||
 
 | 
			
		||||
@@ -23,4 +23,5 @@ module envoy.client {
 | 
			
		||||
	opens envoy.client.ui.custom to javafx.graphics, javafx.fxml;
 | 
			
		||||
	opens envoy.client.ui.settings to envoy.client.util;
 | 
			
		||||
	opens envoy.client.net to dev.kske.eventbus;
 | 
			
		||||
	opens envoy.client.data to dev.kske.eventbus;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -35,6 +35,13 @@ public final class HandshakeRejection extends Event<String> {
 | 
			
		||||
	 */
 | 
			
		||||
	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
 | 
			
		||||
	 * reason.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										26
									
								
								common/src/main/java/envoy/event/NewAuthToken.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								common/src/main/java/envoy/event/NewAuthToken.java
									
									
									
									
									
										Normal 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);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -35,6 +35,8 @@ public final class ServerConfig extends Config {
 | 
			
		||||
		put("enableIssueReporting", "e-ir", Boolean::parseBoolean);
 | 
			
		||||
		put("enableGroups", "e-g", 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
 | 
			
		||||
	 */
 | 
			
		||||
	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(); }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,26 +5,18 @@ import static envoy.data.User.UserStatus.ONLINE;
 | 
			
		||||
import static envoy.event.HandshakeRejection.*;
 | 
			
		||||
 | 
			
		||||
import java.time.Instant;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.HashSet;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.time.temporal.ChronoUnit;
 | 
			
		||||
import java.util.*;
 | 
			
		||||
import java.util.logging.Logger;
 | 
			
		||||
 | 
			
		||||
import javax.persistence.NoResultException;
 | 
			
		||||
 | 
			
		||||
import envoy.data.LoginCredentials;
 | 
			
		||||
import envoy.event.GroupMessageStatusChange;
 | 
			
		||||
import envoy.event.HandshakeRejection;
 | 
			
		||||
import envoy.event.MessageStatusChange;
 | 
			
		||||
import envoy.server.data.GroupMessage;
 | 
			
		||||
import envoy.server.data.PersistenceManager;
 | 
			
		||||
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;
 | 
			
		||||
import envoy.event.*;
 | 
			
		||||
import envoy.server.data.*;
 | 
			
		||||
import envoy.server.net.*;
 | 
			
		||||
import envoy.server.util.*;
 | 
			
		||||
import envoy.util.*;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This {@link ObjectProcessor} handles {@link LoginCredentials}.<br>
 | 
			
		||||
@@ -62,24 +54,39 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
 | 
			
		||||
			try {
 | 
			
		||||
				user = persistenceManager.getUserByName(credentials.getIdentifier());
 | 
			
		||||
 | 
			
		||||
				// Checking if user is already online
 | 
			
		||||
				// Check if the user is already online
 | 
			
		||||
				if (connectionManager.isOnline(user.getID())) {
 | 
			
		||||
					logger.warning(user + " is already online!");
 | 
			
		||||
					writeProxy.write(socketID, new HandshakeRejection(INTERNAL_ERROR));
 | 
			
		||||
					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())) {
 | 
			
		||||
						logger.info(user + " has entered the wrong password.");
 | 
			
		||||
						writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER));
 | 
			
		||||
						return;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			} catch (NoResultException e) {
 | 
			
		||||
				logger.info("The requested user does not exist.");
 | 
			
		||||
				writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER));
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
 | 
			
		||||
			// Validate user name
 | 
			
		||||
			if (!Bounds.isValidContactName(credentials.getIdentifier())) {
 | 
			
		||||
				logger.info("The requested user name is not valid.");
 | 
			
		||||
@@ -87,7 +94,8 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
			try {
 | 
			
		||||
				// Checking that no user already has this identifier
 | 
			
		||||
 | 
			
		||||
				// Check if the name is taken
 | 
			
		||||
				PersistenceManager.getInstance().getUserByName(credentials.getIdentifier());
 | 
			
		||||
 | 
			
		||||
				// This code only gets executed if this user already exists
 | 
			
		||||
@@ -114,6 +122,15 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
 | 
			
		||||
		user.setStatus(ONLINE);
 | 
			
		||||
		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());
 | 
			
		||||
		pendingMessages.removeIf(GroupMessage.class::isInstance);
 | 
			
		||||
		logger.fine("Sending " + pendingMessages.size() + " pending messages to " + user + "...");
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -10,3 +10,4 @@ featureLabel=119
 | 
			
		||||
issueAuthToken=
 | 
			
		||||
consoleLevelBarrier=FINEST
 | 
			
		||||
fileLevelBarrier=WARNING
 | 
			
		||||
authTokenExpiration=90
 | 
			
		||||
		Reference in New Issue
	
	Block a user