Merge pull request 'Token Based Authentication' (#30) from f/token-based-authentication into develop
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/30 Reviewed-by: delvh <leon@kske.dev>
This commit is contained in:
		@@ -35,8 +35,6 @@ public final class ClientConfig extends Config {
 | 
				
			|||||||
		put("server", "s", identity());
 | 
							put("server", "s", identity());
 | 
				
			||||||
		put("port", "p", Integer::parseInt);
 | 
							put("port", "p", Integer::parseInt);
 | 
				
			||||||
		put("localDB", "db", File::new);
 | 
							put("localDB", "db", File::new);
 | 
				
			||||||
		put("user", "u", identity());
 | 
					 | 
				
			||||||
		put("password", "pw", identity());
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
@@ -56,28 +54,4 @@ public final class ClientConfig extends Config {
 | 
				
			|||||||
	 * @since Envoy Client v0.1-alpha
 | 
						 * @since Envoy Client v0.1-alpha
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public File getLocalDB() { return (File) items.get("localDB").get(); }
 | 
						public File getLocalDB() { return (File) items.get("localDB").get(); }
 | 
				
			||||||
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * @return the user name
 | 
					 | 
				
			||||||
	 * @since Envoy Client v0.3-alpha
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	public String getUser() {
 | 
					 | 
				
			||||||
		final String user = (String) items.get("user").get();
 | 
					 | 
				
			||||||
		return user.equals("") ? null : user;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * @return the password
 | 
					 | 
				
			||||||
	 * @since Envoy Client v0.3-alpha
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	public String getPassword() {
 | 
					 | 
				
			||||||
		final String password = (String) items.get("password").get();
 | 
					 | 
				
			||||||
		return password.equals("") ? null : password;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/**
 | 
					 | 
				
			||||||
	 * @return {@code true} if user name and password are set
 | 
					 | 
				
			||||||
	 * @since Envoy Client v0.3-alpha
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	public boolean hasLoginCredentials() { return getUser() != null && getPassword() != null; }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,10 +131,24 @@ 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.
 | 
				
			||||||
	 * 
 | 
						 *
 | 
				
			||||||
	 * @since Envoy Client v0.1-beta
 | 
						 * @since Envoy Client v0.1-beta
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public void synchronize() {
 | 
						public void synchronize() {
 | 
				
			||||||
@@ -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.
 | 
				
			||||||
	 *
 | 
						 *
 | 
				
			||||||
@@ -217,7 +253,7 @@ public final class LocalDB {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Searches for a chat by recipient ID.
 | 
						 * Searches for a chat by recipient ID.
 | 
				
			||||||
	 * 
 | 
						 *
 | 
				
			||||||
	 * @param recipientID the ID of the chat's recipient
 | 
						 * @param recipientID the ID of the chat's recipient
 | 
				
			||||||
	 * @return an optional containing the chat
 | 
						 * @return an optional containing the chat
 | 
				
			||||||
	 * @since Envoy Client v0.1-beta
 | 
						 * @since Envoy Client v0.1-beta
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -77,27 +77,40 @@ public final class Startup extends Application {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		// Prepare handshake
 | 
							// Prepare handshake
 | 
				
			||||||
		localDB.loadIDGenerator();
 | 
							localDB.loadIDGenerator();
 | 
				
			||||||
 | 
							localDB.loadLastLogin();
 | 
				
			||||||
		context.setLocalDB(localDB);
 | 
							context.setLocalDB(localDB);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Configure stage
 | 
				
			||||||
		stage.setTitle("Envoy");
 | 
							stage.setTitle("Envoy");
 | 
				
			||||||
		stage.getIcons().add(IconUtil.loadIcon("envoy_logo"));
 | 
							stage.getIcons().add(IconUtil.loadIcon("envoy_logo"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Create scene context
 | 
				
			||||||
		final var sceneContext = new SceneContext(stage);
 | 
							final var sceneContext = new SceneContext(stage);
 | 
				
			||||||
		context.setSceneContext(sceneContext);
 | 
							context.setSceneContext(sceneContext);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Perform automatic login if configured
 | 
							// Authenticate with token if present
 | 
				
			||||||
		if (config.hasLoginCredentials())
 | 
							if (localDB.getAuthToken() != null) {
 | 
				
			||||||
			performHandshake(new LoginCredentials(config.getUser(), config.getPassword(), false, Startup.VERSION, loadLastSync(config.getUser())));
 | 
								logger.info("Attempting authentication with token...");
 | 
				
			||||||
		else sceneContext.load(SceneInfo.LOGIN_SCENE);
 | 
								localDB.initializeUserStorage();
 | 
				
			||||||
 | 
								localDB.loadUserData();
 | 
				
			||||||
 | 
								if (!performHandshake(
 | 
				
			||||||
 | 
										LoginCredentials.loginWithToken(localDB.getUser().getName(), localDB.getAuthToken(), VERSION, localDB.getLastSync())))
 | 
				
			||||||
 | 
									sceneContext.load(SceneInfo.LOGIN_SCENE);
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Load login scene
 | 
				
			||||||
 | 
								sceneContext.load(SceneInfo.LOGIN_SCENE);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Tries to perform a Handshake with the server.
 | 
						 * Tries to perform a Handshake with the server.
 | 
				
			||||||
	 *
 | 
						 *
 | 
				
			||||||
	 * @param credentials the credentials to use for the handshake
 | 
						 * @param credentials the credentials to use for the handshake
 | 
				
			||||||
 | 
						 * @return whether the handshake was successful or offline mode could be entered
 | 
				
			||||||
	 * @since Envoy Client v0.2-beta
 | 
						 * @since Envoy Client v0.2-beta
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public static void performHandshake(LoginCredentials credentials) {
 | 
						public static boolean performHandshake(LoginCredentials credentials) {
 | 
				
			||||||
		final var cacheMap = new CacheMap();
 | 
							final var cacheMap = new CacheMap();
 | 
				
			||||||
		cacheMap.put(Message.class, new Cache<Message>());
 | 
							cacheMap.put(Message.class, new Cache<Message>());
 | 
				
			||||||
		cacheMap.put(GroupMessage.class, new Cache<GroupMessage>());
 | 
							cacheMap.put(GroupMessage.class, new Cache<GroupMessage>());
 | 
				
			||||||
@@ -109,10 +122,13 @@ public final class Startup extends Application {
 | 
				
			|||||||
			if (client.isOnline()) {
 | 
								if (client.isOnline()) {
 | 
				
			||||||
				loadChatScene();
 | 
									loadChatScene();
 | 
				
			||||||
				client.initReceiver(localDB, cacheMap);
 | 
									client.initReceiver(localDB, cacheMap);
 | 
				
			||||||
 | 
									return true;
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									return false;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		} catch (IOException | InterruptedException | TimeoutException e) {
 | 
							} catch (IOException | InterruptedException | TimeoutException e) {
 | 
				
			||||||
			logger.log(Level.INFO, "Could not connect to server. Entering offline mode...");
 | 
								logger.log(Level.INFO, "Could not connect to server. Entering offline mode...");
 | 
				
			||||||
			attemptOfflineMode(credentials.getIdentifier());
 | 
								return attemptOfflineMode(credentials.getIdentifier());
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -121,9 +137,10 @@ public final class Startup extends Application {
 | 
				
			|||||||
	 * for a given user.
 | 
						 * for a given user.
 | 
				
			||||||
	 *
 | 
						 *
 | 
				
			||||||
	 * @param identifier the identifier of the user - currently his username
 | 
						 * @param identifier the identifier of the user - currently his username
 | 
				
			||||||
 | 
						 * @return whether the offline mode could be entered
 | 
				
			||||||
	 * @since Envoy Client v0.2-beta
 | 
						 * @since Envoy Client v0.2-beta
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public static void attemptOfflineMode(String identifier) {
 | 
						public static boolean attemptOfflineMode(String identifier) {
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			// Try entering offline mode
 | 
								// Try entering offline mode
 | 
				
			||||||
			localDB.loadUsers();
 | 
								localDB.loadUsers();
 | 
				
			||||||
@@ -131,10 +148,12 @@ public final class Startup extends Application {
 | 
				
			|||||||
			if (clientUser == null) throw new EnvoyException("Could not enter offline mode: user name unknown");
 | 
								if (clientUser == null) throw new EnvoyException("Could not enter offline mode: user name unknown");
 | 
				
			||||||
			client.setSender(clientUser);
 | 
								client.setSender(clientUser);
 | 
				
			||||||
			loadChatScene();
 | 
								loadChatScene();
 | 
				
			||||||
 | 
								return true;
 | 
				
			||||||
		} catch (final Exception e) {
 | 
							} catch (final Exception e) {
 | 
				
			||||||
			new Alert(AlertType.ERROR, "Client error: " + e).showAndWait();
 | 
								new Alert(AlertType.ERROR, "Client error: " + e).showAndWait();
 | 
				
			||||||
			logger.log(Level.SEVERE, "Offline mode could not be loaded: ", e);
 | 
								logger.log(Level.SEVERE, "Offline mode could not be loaded: ", e);
 | 
				
			||||||
			System.exit(1);
 | 
								System.exit(1);
 | 
				
			||||||
 | 
								return false;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
package envoy.client.ui.controller;
 | 
					package envoy.client.ui.controller;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.time.Instant;
 | 
				
			||||||
import java.util.logging.*;
 | 
					import java.util.logging.*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import javafx.fxml.FXML;
 | 
					import javafx.fxml.FXML;
 | 
				
			||||||
@@ -45,6 +46,9 @@ public final class LoginScene implements EventListener {
 | 
				
			|||||||
	@FXML
 | 
						@FXML
 | 
				
			||||||
	private Button loginButton;
 | 
						private Button loginButton;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@FXML
 | 
				
			||||||
 | 
						private CheckBox cbStaySignedIn;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@FXML
 | 
						@FXML
 | 
				
			||||||
	private Button offlineModeButton;
 | 
						private Button offlineModeButton;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -54,10 +58,10 @@ public final class LoginScene implements EventListener {
 | 
				
			|||||||
	@FXML
 | 
						@FXML
 | 
				
			||||||
	private ImageView logo;
 | 
						private ImageView logo;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private boolean registration = false;
 | 
						private boolean registration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private static final Logger			logger		= EnvoyLog.getLogger(LoginScene.class);
 | 
						private static final Logger			logger	= EnvoyLog.getLogger(LoginScene.class);
 | 
				
			||||||
	private static final ClientConfig	config		= ClientConfig.getInstance();
 | 
						private static final ClientConfig	config	= ClientConfig.getInstance();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@FXML
 | 
						@FXML
 | 
				
			||||||
	private void initialize() {
 | 
						private void initialize() {
 | 
				
			||||||
@@ -74,16 +78,21 @@ public final class LoginScene implements EventListener {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	@FXML
 | 
						@FXML
 | 
				
			||||||
	private void loginButtonPressed() {
 | 
						private void loginButtonPressed() {
 | 
				
			||||||
 | 
							final String user = userTextField.getText(), pass = passwordField.getText(), repeatPass = repeatPasswordField.getText();
 | 
				
			||||||
 | 
							final boolean	requestToken	= cbStaySignedIn.isSelected();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Prevent registration with unequal passwords
 | 
							// Prevent registration with unequal passwords
 | 
				
			||||||
		if (registration && !passwordField.getText().equals(repeatPasswordField.getText())) {
 | 
							if (registration && !pass.equals(repeatPass)) {
 | 
				
			||||||
			new Alert(AlertType.ERROR, "The entered password is unequal to the repeated one").showAndWait();
 | 
								new Alert(AlertType.ERROR, "The entered password is unequal to the repeated one").showAndWait();
 | 
				
			||||||
			repeatPasswordField.clear();
 | 
								repeatPasswordField.clear();
 | 
				
			||||||
		} else if (!Bounds.isValidContactName(userTextField.getText())) {
 | 
							} else if (!Bounds.isValidContactName(user)) {
 | 
				
			||||||
			new Alert(AlertType.ERROR, "The entered user name is not valid (" + Bounds.CONTACT_NAME_PATTERN + ")").showAndWait();
 | 
								new Alert(AlertType.ERROR, "The entered user name is not valid (" + Bounds.CONTACT_NAME_PATTERN + ")").showAndWait();
 | 
				
			||||||
			userTextField.clear();
 | 
								userTextField.clear();
 | 
				
			||||||
		} else Startup.performHandshake(new LoginCredentials(userTextField.getText(), passwordField.getText(), registration, Startup.VERSION,
 | 
							} else {
 | 
				
			||||||
				Startup.loadLastSync(userTextField.getText())));
 | 
								Instant lastSync = Startup.loadLastSync(userTextField.getText());
 | 
				
			||||||
 | 
								Startup.performHandshake(registration ? LoginCredentials.registration(user, pass, requestToken, Startup.VERSION, lastSync)
 | 
				
			||||||
 | 
										: LoginCredentials.login(user, pass, requestToken, Startup.VERSION, lastSync));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@FXML
 | 
						@FXML
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<?import javafx.geometry.Insets?>
 | 
					<?import javafx.geometry.Insets?>
 | 
				
			||||||
<?import javafx.scene.control.Button?>
 | 
					<?import javafx.scene.control.Button?>
 | 
				
			||||||
 | 
					<?import javafx.scene.control.CheckBox?>
 | 
				
			||||||
<?import javafx.scene.control.Label?>
 | 
					<?import javafx.scene.control.Label?>
 | 
				
			||||||
<?import javafx.scene.control.PasswordField?>
 | 
					<?import javafx.scene.control.PasswordField?>
 | 
				
			||||||
<?import javafx.scene.control.TextField?>
 | 
					<?import javafx.scene.control.TextField?>
 | 
				
			||||||
@@ -32,9 +33,7 @@
 | 
				
			|||||||
            <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
 | 
					            <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
 | 
				
			||||||
         </padding>
 | 
					         </padding>
 | 
				
			||||||
      </Label>
 | 
					      </Label>
 | 
				
			||||||
		<Label alignment="TOP_CENTER" contentDisplay="CENTER"
 | 
							<Label alignment="TOP_CENTER" contentDisplay="CENTER" prefHeight="33.0" prefWidth="110.0" text="LOGIN" textAlignment="CENTER">
 | 
				
			||||||
			prefHeight="33.0" prefWidth="110.0" text="LOGIN"
 | 
					 | 
				
			||||||
			textAlignment="CENTER">
 | 
					 | 
				
			||||||
			<font>
 | 
								<font>
 | 
				
			||||||
				<Font size="26.0" />
 | 
									<Font size="26.0" />
 | 
				
			||||||
			</font>
 | 
								</font>
 | 
				
			||||||
@@ -92,6 +91,11 @@
 | 
				
			|||||||
            <Insets bottom="2.0" left="125.0" right="125.0" top="2.0" />
 | 
					            <Insets bottom="2.0" left="125.0" right="125.0" top="2.0" />
 | 
				
			||||||
         </padding>
 | 
					         </padding>
 | 
				
			||||||
				</Button>
 | 
									</Button>
 | 
				
			||||||
 | 
					      <CheckBox fx:id="cbStaySignedIn" mnemonicParsing="false" text="Keep me signed in">
 | 
				
			||||||
 | 
					         <VBox.margin>
 | 
				
			||||||
 | 
					            <Insets bottom="10.0" top="10.0" />
 | 
				
			||||||
 | 
					         </VBox.margin>
 | 
				
			||||||
 | 
					      </CheckBox>
 | 
				
			||||||
      <HBox alignment="CENTER" prefHeight="30.0" prefWidth="200.0">
 | 
					      <HBox alignment="CENTER" prefHeight="30.0" prefWidth="200.0">
 | 
				
			||||||
         <children>
 | 
					         <children>
 | 
				
			||||||
            <Label fx:id="registerTextLabel" text="No account yet?" />
 | 
					            <Label fx:id="registerTextLabel" text="No account yet?" />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,9 @@ import java.time.Instant;
 | 
				
			|||||||
 * Contains a {@link User}'s login / registration information as well as the
 | 
					 * Contains a {@link User}'s login / registration information as well as the
 | 
				
			||||||
 * client version.
 | 
					 * client version.
 | 
				
			||||||
 * <p>
 | 
					 * <p>
 | 
				
			||||||
 | 
					 * If the authentication is performed with a token, the token is stored instead
 | 
				
			||||||
 | 
					 * of the password.
 | 
				
			||||||
 | 
					 * <p>
 | 
				
			||||||
 * Project: <strong>envoy-common</strong><br>
 | 
					 * Project: <strong>envoy-common</strong><br>
 | 
				
			||||||
 * File: <strong>LoginCredentials.java</strong><br>
 | 
					 * File: <strong>LoginCredentials.java</strong><br>
 | 
				
			||||||
 * Created: <strong>29.12.2019</strong><br>
 | 
					 * Created: <strong>29.12.2019</strong><br>
 | 
				
			||||||
@@ -17,35 +20,73 @@ import java.time.Instant;
 | 
				
			|||||||
public final class LoginCredentials implements Serializable {
 | 
					public final class LoginCredentials implements Serializable {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private final String	identifier, password, clientVersion;
 | 
						private final String	identifier, password, clientVersion;
 | 
				
			||||||
	private final boolean	registration;
 | 
						private final boolean	registration, token, requestToken;
 | 
				
			||||||
	private final Instant	lastSync;
 | 
						private final Instant	lastSync;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private static final long serialVersionUID = 3;
 | 
						private static final long serialVersionUID = 4;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						private LoginCredentials(String identifier, String password, boolean registration, boolean token, boolean requestToken, String clientVersion,
 | 
				
			||||||
	 * Initializes login credentials for a handshake.
 | 
								Instant lastSync) {
 | 
				
			||||||
	 *
 | 
					 | 
				
			||||||
	 * @param identifier    the identifier of the user
 | 
					 | 
				
			||||||
	 * @param password      the password of the user
 | 
					 | 
				
			||||||
	 * @param registration  signifies that these credentials are used for user
 | 
					 | 
				
			||||||
	 *                      registration instead of user login
 | 
					 | 
				
			||||||
	 * @param clientVersion the version of the client sending these credentials
 | 
					 | 
				
			||||||
	 * @param lastSync      the time stamp of the last synchronization
 | 
					 | 
				
			||||||
	 * @since Envoy Common v0.2-beta
 | 
					 | 
				
			||||||
	 */
 | 
					 | 
				
			||||||
	public LoginCredentials(String identifier, String password, boolean registration, String clientVersion, Instant lastSync) {
 | 
					 | 
				
			||||||
		this.identifier		= identifier;
 | 
							this.identifier		= identifier;
 | 
				
			||||||
		this.password		= password;
 | 
							this.password		= password;
 | 
				
			||||||
		this.registration	= registration;
 | 
							this.registration	= registration;
 | 
				
			||||||
 | 
							this.token			= token;
 | 
				
			||||||
 | 
							this.requestToken	= requestToken;
 | 
				
			||||||
		this.clientVersion	= clientVersion;
 | 
							this.clientVersion	= clientVersion;
 | 
				
			||||||
		this.lastSync		= lastSync;
 | 
							this.lastSync		= lastSync;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Creates login credentials for a regular login.
 | 
				
			||||||
 | 
						 *
 | 
				
			||||||
 | 
						 * @param identifier    the identifier of the user
 | 
				
			||||||
 | 
						 * @param password      the password of the user
 | 
				
			||||||
 | 
						 * @param requestToken  requests the server to generate an authentication token
 | 
				
			||||||
 | 
						 * @param clientVersion the version of the client sending these credentials
 | 
				
			||||||
 | 
						 * @param lastSync      the timestamp of the last synchronization
 | 
				
			||||||
 | 
						 * @return the created login credentials
 | 
				
			||||||
 | 
						 * @since Envoy Common v0.2-beta
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						public static LoginCredentials login(String identifier, String password, boolean requestToken, String clientVersion, Instant lastSync) {
 | 
				
			||||||
 | 
							return new LoginCredentials(identifier, password, false, false, requestToken, clientVersion, lastSync);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Creates login credentials for a login with an authentication token.
 | 
				
			||||||
 | 
						 *
 | 
				
			||||||
 | 
						 * @param identifier    the identifier of the user
 | 
				
			||||||
 | 
						 * @param token         the authentication token of the user
 | 
				
			||||||
 | 
						 * @param clientVersion the version of the client sending these credentials
 | 
				
			||||||
 | 
						 * @param lastSync      the timestamp of the last synchronization
 | 
				
			||||||
 | 
						 * @return the created login credentials
 | 
				
			||||||
 | 
						 * @since Envoy Common v0.2-beta
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						public static LoginCredentials loginWithToken(String identifier, String token, String clientVersion, Instant lastSync) {
 | 
				
			||||||
 | 
							return new LoginCredentials(identifier, token, false, true, false, clientVersion, lastSync);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Creates login credentials for a registration.
 | 
				
			||||||
 | 
						 *
 | 
				
			||||||
 | 
						 * @param identifier    the identifier of the user
 | 
				
			||||||
 | 
						 * @param password      the password of the user
 | 
				
			||||||
 | 
						 * @param requestToken  requests the server to generate an authentication token
 | 
				
			||||||
 | 
						 * @param clientVersion the version of the client sending these credentials
 | 
				
			||||||
 | 
						 * @param lastSync      the timestamp of the last synchronization
 | 
				
			||||||
 | 
						 * @return the created login credentials
 | 
				
			||||||
 | 
						 * @since Envoy Common v0.2-beta
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						public static LoginCredentials registration(String identifier, String password, boolean requestToken, String clientVersion, Instant lastSync) {
 | 
				
			||||||
 | 
							return new LoginCredentials(identifier, password, true, false, requestToken, clientVersion, lastSync);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@Override
 | 
						@Override
 | 
				
			||||||
	public String toString() {
 | 
						public String toString() {
 | 
				
			||||||
		return String.format("LoginCredentials[identifier=%s,registration=%b,clientVersion=%s,lastSync=%s]",
 | 
							return String.format("LoginCredentials[identifier=%s,registration=%b,token=%b,requestToken=%b,clientVersion=%s,lastSync=%s]",
 | 
				
			||||||
				identifier,
 | 
									identifier,
 | 
				
			||||||
				registration,
 | 
									registration,
 | 
				
			||||||
 | 
									token,
 | 
				
			||||||
 | 
									requestToken,
 | 
				
			||||||
				clientVersion,
 | 
									clientVersion,
 | 
				
			||||||
				lastSync);
 | 
									lastSync);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -69,6 +110,19 @@ public final class LoginCredentials implements Serializable {
 | 
				
			|||||||
	 */
 | 
						 */
 | 
				
			||||||
	public boolean isRegistration() { return registration; }
 | 
						public boolean isRegistration() { return registration; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * @return {@code true} if these credentials use an authentication token instead
 | 
				
			||||||
 | 
						 *         of a password
 | 
				
			||||||
 | 
						 * @since Envoy Common v0.2-beta
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						public boolean usesToken() { return token; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * @return {@code true} if the server should generate a new authentication token
 | 
				
			||||||
 | 
						 * @since Envoy Common v0.2-beta
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						public boolean requestToken() { return requestToken; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * @return the version of the client sending these credentials
 | 
						 * @return the version of the client sending these credentials
 | 
				
			||||||
	 * @since Envoy Common v0.1-beta
 | 
						 * @since Envoy Common v0.1-beta
 | 
				
			||||||
@@ -76,7 +130,7 @@ public final class LoginCredentials implements Serializable {
 | 
				
			|||||||
	public String getClientVersion() { return clientVersion; }
 | 
						public String getClientVersion() { return clientVersion; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * @return the time stamp of the last synchronization
 | 
						 * @return the timestamp of the last synchronization
 | 
				
			||||||
	 * @since Envoy Common v0.2-beta
 | 
						 * @since Envoy Common v0.2-beta
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public Instant getLastSync() { return lastSync; }
 | 
						public Instant getLastSync() { return lastSync; }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,14 +15,14 @@ public final class HandshakeRejection extends Event<String> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Select this value if a given password hash or user name was incorrect.
 | 
						 * Select this value if a given password hash or user name was incorrect.
 | 
				
			||||||
	 * 
 | 
						 *
 | 
				
			||||||
	 * @since Envoy Common v0.3-alpha
 | 
						 * @since Envoy Common v0.3-alpha
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public static final String WRONG_PASSWORD_OR_USER = "Incorrect user name or password.";
 | 
						public static final String WRONG_PASSWORD_OR_USER = "Incorrect user name or password.";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Select this value if a given user name for a registration is already taken.
 | 
						 * Select this value if a given user name for a registration is already taken.
 | 
				
			||||||
	 * 
 | 
						 *
 | 
				
			||||||
	 * @since Envoy Common v0.1-beta
 | 
						 * @since Envoy Common v0.1-beta
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public static final String USERNAME_TAKEN = "Incorrect user name or password.";
 | 
						public static final String USERNAME_TAKEN = "Incorrect user name or password.";
 | 
				
			||||||
@@ -30,15 +30,22 @@ public final class HandshakeRejection extends Event<String> {
 | 
				
			|||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Select this value if the version of the client is incompatible with the
 | 
						 * Select this value if the version of the client is incompatible with the
 | 
				
			||||||
	 * server.
 | 
						 * server.
 | 
				
			||||||
	 * 
 | 
						 *
 | 
				
			||||||
	 * @since Envoy Common v0.1-beta
 | 
						 * @since Envoy Common v0.1-beta
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	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.
 | 
				
			||||||
	 * 
 | 
						 *
 | 
				
			||||||
	 * @since Envoy Common v0.3-alpha
 | 
						 * @since Envoy Common v0.3-alpha
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public static final String INTERNAL_ERROR = "An internal error occured.";
 | 
						public static final String INTERNAL_ERROR = "An internal error occured.";
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										29
									
								
								common/src/main/java/envoy/event/NewAuthToken.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								common/src/main/java/envoy/event/NewAuthToken.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					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);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public String toString() { return "NewAuthToken"; }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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(); }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -40,7 +40,7 @@ public final class User extends Contact {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Named query retrieving a user by name (parameter {@code :name}).
 | 
						 * Named query retrieving a user by name (parameter {@code :name}).
 | 
				
			||||||
	 * 
 | 
						 *
 | 
				
			||||||
	 * @since Envoy Server Standalone v0.1-beta
 | 
						 * @since Envoy Server Standalone v0.1-beta
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public static final String findByName = "User.findByName";
 | 
						public static final String findByName = "User.findByName";
 | 
				
			||||||
@@ -48,7 +48,7 @@ public final class User extends Contact {
 | 
				
			|||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Named query retrieving the contacts of a given user (parameter
 | 
						 * Named query retrieving the contacts of a given user (parameter
 | 
				
			||||||
	 * {@code :user}).
 | 
						 * {@code :user}).
 | 
				
			||||||
	 * 
 | 
						 *
 | 
				
			||||||
	 * @since Envoy Server Standalone v0.1-beta
 | 
						 * @since Envoy Server Standalone v0.1-beta
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public static final String findContacts = "User.findContacts";
 | 
						public static final String findContacts = "User.findContacts";
 | 
				
			||||||
@@ -57,7 +57,7 @@ public final class User extends Contact {
 | 
				
			|||||||
	 * Named query searching for users with a name like a search phrase (parameter
 | 
						 * Named query searching for users with a name like a search phrase (parameter
 | 
				
			||||||
	 * {@code :searchPhrase}) that are not in the contact list of a given user
 | 
						 * {@code :searchPhrase}) that are not in the contact list of a given user
 | 
				
			||||||
	 * (parameter {@code :context}).
 | 
						 * (parameter {@code :context}).
 | 
				
			||||||
	 * 
 | 
						 *
 | 
				
			||||||
	 * @since Envoy Server Standalone v0.1-beta
 | 
						 * @since Envoy Server Standalone v0.1-beta
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public static final String searchByName = "User.searchByName";
 | 
						public static final String searchByName = "User.searchByName";
 | 
				
			||||||
@@ -65,6 +65,12 @@ public final class User extends Contact {
 | 
				
			|||||||
	@Column(name = "password_hash")
 | 
						@Column(name = "password_hash")
 | 
				
			||||||
	private String passwordHash;
 | 
						private String passwordHash;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Column(name = "auth_token")
 | 
				
			||||||
 | 
						private String authToken;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Column(name = "auth_token_expiration")
 | 
				
			||||||
 | 
						private Instant authTokenExpiration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@Column(name = "last_seen")
 | 
						@Column(name = "last_seen")
 | 
				
			||||||
	private Instant lastSeen;
 | 
						private Instant lastSeen;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -90,6 +96,31 @@ public final class User extends Contact {
 | 
				
			|||||||
	 */
 | 
						 */
 | 
				
			||||||
	public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }
 | 
						public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * @return the authentication token
 | 
				
			||||||
 | 
						 * @since Envoy Server v0.2-beta
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						public String getAuthToken() { return authToken; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * @param authToken the authentication token to set
 | 
				
			||||||
 | 
						 * @since Envoy Server v0.2-beta
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						public void setAuthToken(String authToken) { this.authToken = authToken; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * @return the time at which the authentication token expires
 | 
				
			||||||
 | 
						 * @since Envoy Server v0.2-beta
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						public Instant getAuthTokenExpiration() { return authTokenExpiration; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * @param authTokenExpiration the authentication token expiration timestamp to
 | 
				
			||||||
 | 
						 *                            set
 | 
				
			||||||
 | 
						 * @since Envoy Server v0.2-beta
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						public void setAuthTokenExpiration(Instant authTokenExpiration) { this.authTokenExpiration = authTokenExpiration; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * @return the last date the user has been online
 | 
						 * @return the last date the user has been online
 | 
				
			||||||
	 * @since Envoy Server Standalone v0.2-beta
 | 
						 * @since Envoy Server Standalone v0.2-beta
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,17 +54,31 @@ 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
 | 
					
 | 
				
			||||||
				if (!PasswordUtil.validate(credentials.getPassword(), user.getPasswordHash())) {
 | 
									// Authenticate with password or token
 | 
				
			||||||
					logger.info(user + " has entered the wrong password.");
 | 
									if (credentials.usesToken()) {
 | 
				
			||||||
					writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER));
 | 
					
 | 
				
			||||||
					return;
 | 
										// 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) {
 | 
								} catch (NoResultException e) {
 | 
				
			||||||
				logger.info("The requested user does not exist.");
 | 
									logger.info("The requested user does not exist.");
 | 
				
			||||||
@@ -80,6 +86,7 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
 | 
				
			|||||||
				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,25 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
 | 
				
			|||||||
		user.setStatus(ONLINE);
 | 
							user.setStatus(ONLINE);
 | 
				
			||||||
		UserStatusChangeProcessor.updateUserStatus(user);
 | 
							UserStatusChangeProcessor.updateUserStatus(user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Process token request
 | 
				
			||||||
 | 
							if (credentials.requestToken()) {
 | 
				
			||||||
 | 
								String token;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (user.getAuthToken() != null && user.getAuthTokenExpiration().isAfter(Instant.now())) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Reuse existing token and delay expiration date
 | 
				
			||||||
 | 
									token = user.getAuthToken();
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Generate new token
 | 
				
			||||||
 | 
									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 + "...");
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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=
 | 
					issueAuthToken=
 | 
				
			||||||
consoleLevelBarrier=FINEST
 | 
					consoleLevelBarrier=FINEST
 | 
				
			||||||
fileLevelBarrier=WARNING
 | 
					fileLevelBarrier=WARNING
 | 
				
			||||||
 | 
					authTokenExpiration=90
 | 
				
			||||||
		Reference in New Issue
	
	Block a user