From f21d0775223a80c294f63b7b57a2a57a6cf75620 Mon Sep 17 00:00:00 2001 From: kske Date: Sat, 19 Sep 2020 11:37:42 +0200 Subject: [PATCH] Add token-based authentication (without rejection handling) --- .../main/java/envoy/client/data/LocalDB.java | 44 +++++++++++-- .../main/java/envoy/client/net/Client.java | 7 ++- .../main/java/envoy/client/ui/Startup.java | 15 ++++- client/src/main/java/module-info.java | 1 + .../java/envoy/event/HandshakeRejection.java | 15 +++-- .../main/java/envoy/event/NewAuthToken.java | 26 ++++++++ .../java/envoy/server/data/ServerConfig.java | 8 +++ .../processors/LoginCredentialProcessor.java | 61 ++++++++++++------- .../envoy/server/util/AuthTokenGenerator.java | 35 +++++++++++ server/src/main/resources/server.properties | 1 + 10 files changed, 178 insertions(+), 35 deletions(-) create mode 100644 common/src/main/java/envoy/event/NewAuthToken.java create mode 100644 server/src/main/java/envoy/server/util/AuthTokenGenerator.java diff --git a/client/src/main/java/envoy/client/data/LocalDB.java b/client/src/main/java/envoy/client/data/LocalDB.java index d44caa0..63e567a 100644 --- a/client/src/main/java/envoy/client/data/LocalDB.java +++ b/client/src/main/java/envoy/client/data/LocalDB.java @@ -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 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,10 +131,24 @@ 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. - * + * * @since Envoy Client v0.1-beta */ public void synchronize() { @@ -143,6 +168,11 @@ public final class LocalDB { .forEach(chats::add); } + @Event + private void onNewAuthToken(NewAuthToken evt) { + authToken = evt.get(); + } + /** * @return a {@code Map} 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. * @@ -217,7 +253,7 @@ public final class LocalDB { /** * Searches for a chat by recipient ID. - * + * * @param recipientID the ID of the chat's recipient * @return an optional containing the chat * @since Envoy Client v0.1-beta diff --git a/client/src/main/java/envoy/client/net/Client.java b/client/src/main/java/envoy/client/net/Client.java index 0db67eb..9e41343 100644 --- a/client/src/main/java/envoy/client/net/Client.java +++ b/client/src/main/java/envoy/client/net/Client.java @@ -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 diff --git a/client/src/main/java/envoy/client/ui/Startup.java b/client/src/main/java/envoy/client/ui/Startup.java index a9103c9..c3dea29 100644 --- a/client/src/main/java/envoy/client/ui/Startup.java +++ b/client/src/main/java/envoy/client/ui/Startup.java @@ -77,6 +77,7 @@ public final class Startup extends Application { // Prepare handshake localDB.loadIDGenerator(); + localDB.loadLastLogin(); context.setLocalDB(localDB); // Configure stage @@ -87,8 +88,18 @@ public final class Startup extends Application { final var sceneContext = new SceneContext(stage); context.setSceneContext(sceneContext); - // Load login scene - sceneContext.load(SceneInfo.LOGIN_SCENE); + // 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); + } } /** diff --git a/client/src/main/java/module-info.java b/client/src/main/java/module-info.java index bfcf8d2..4382521 100644 --- a/client/src/main/java/module-info.java +++ b/client/src/main/java/module-info.java @@ -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; } diff --git a/common/src/main/java/envoy/event/HandshakeRejection.java b/common/src/main/java/envoy/event/HandshakeRejection.java index 4024772..b609085 100644 --- a/common/src/main/java/envoy/event/HandshakeRejection.java +++ b/common/src/main/java/envoy/event/HandshakeRejection.java @@ -15,14 +15,14 @@ public final class HandshakeRejection extends Event { /** * Select this value if a given password hash or user name was incorrect. - * + * * @since Envoy Common v0.3-alpha */ 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. - * + * * @since Envoy Common v0.1-beta */ public static final String USERNAME_TAKEN = "Incorrect user name or password."; @@ -30,15 +30,22 @@ public final class HandshakeRejection extends Event { /** * Select this value if the version of the client is incompatible with the * server. - * + * * @since Envoy Common v0.1-beta */ 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. - * + * * @since Envoy Common v0.3-alpha */ public static final String INTERNAL_ERROR = "An internal error occured."; diff --git a/common/src/main/java/envoy/event/NewAuthToken.java b/common/src/main/java/envoy/event/NewAuthToken.java new file mode 100644 index 0000000..9d99c47 --- /dev/null +++ b/common/src/main/java/envoy/event/NewAuthToken.java @@ -0,0 +1,26 @@ +package envoy.event; + +/** + * This event can be used to transmit a new authentication token to a client. + *

+ * Project: envoy-common
+ * File: NewAuthToken.java
+ * Created: 19.09.2020
+ * + * @author Kai S. K. Engelbart + * @since Envoy Common v0.2-beta + */ +public class NewAuthToken extends Event { + + 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); + } +} diff --git a/server/src/main/java/envoy/server/data/ServerConfig.java b/server/src/main/java/envoy/server/data/ServerConfig.java index 150e626..b16733d 100644 --- a/server/src/main/java/envoy/server/data/ServerConfig.java +++ b/server/src/main/java/envoy/server/data/ServerConfig.java @@ -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(); } } diff --git a/server/src/main/java/envoy/server/processors/LoginCredentialProcessor.java b/server/src/main/java/envoy/server/processors/LoginCredentialProcessor.java index 87da681..5f5cf01 100755 --- a/server/src/main/java/envoy/server/processors/LoginCredentialProcessor.java +++ b/server/src/main/java/envoy/server/processors/LoginCredentialProcessor.java @@ -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}.
@@ -62,17 +54,31 @@ public final class LoginCredentialProcessor implements ObjectProcessor + * Project: envoy-server
+ * File: AuthTokenGenerator.java
+ * Created: 19.09.2020
+ * + * @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); + } +} diff --git a/server/src/main/resources/server.properties b/server/src/main/resources/server.properties index c2aee5a..86a8456 100644 --- a/server/src/main/resources/server.properties +++ b/server/src/main/resources/server.properties @@ -10,3 +10,4 @@ featureLabel=119 issueAuthToken= consoleLevelBarrier=FINEST fileLevelBarrier=WARNING +authTokenExpiration=90 \ No newline at end of file