From 89b9afb3dbcede1b101928e1b2d34cffe2a915e3 Mon Sep 17 00:00:00 2001 From: kske Date: Fri, 18 Sep 2020 10:01:57 +0200 Subject: [PATCH 1/6] Remove config based autologin Fixes #27 --- .../java/envoy/client/data/ClientConfig.java | 26 ------------------- .../main/java/envoy/client/ui/Startup.java | 8 +++--- 2 files changed, 4 insertions(+), 30 deletions(-) diff --git a/client/src/main/java/envoy/client/data/ClientConfig.java b/client/src/main/java/envoy/client/data/ClientConfig.java index a7763e0..1d6498e 100644 --- a/client/src/main/java/envoy/client/data/ClientConfig.java +++ b/client/src/main/java/envoy/client/data/ClientConfig.java @@ -35,8 +35,6 @@ public final class ClientConfig extends Config { put("server", "s", identity()); put("port", "p", Integer::parseInt); 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 */ 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; } } diff --git a/client/src/main/java/envoy/client/ui/Startup.java b/client/src/main/java/envoy/client/ui/Startup.java index 30757c5..a9103c9 100644 --- a/client/src/main/java/envoy/client/ui/Startup.java +++ b/client/src/main/java/envoy/client/ui/Startup.java @@ -79,16 +79,16 @@ public final class Startup extends Application { localDB.loadIDGenerator(); context.setLocalDB(localDB); + // Configure stage stage.setTitle("Envoy"); stage.getIcons().add(IconUtil.loadIcon("envoy_logo")); + // Create scene context final var sceneContext = new SceneContext(stage); context.setSceneContext(sceneContext); - // Perform automatic login if configured - if (config.hasLoginCredentials()) - performHandshake(new LoginCredentials(config.getUser(), config.getPassword(), false, Startup.VERSION, loadLastSync(config.getUser()))); - else sceneContext.load(SceneInfo.LOGIN_SCENE); + // Load login scene + sceneContext.load(SceneInfo.LOGIN_SCENE); } /** -- 2.30.2 From ec6b67099f0d0db6d6d22e03da2d12e9c87434f4 Mon Sep 17 00:00:00 2001 From: kske Date: Fri, 18 Sep 2020 11:29:05 +0200 Subject: [PATCH 2/6] Add token to login credentials and database user --- .../client/ui/controller/LoginScene.java | 17 +++-- .../java/envoy/data/LoginCredentials.java | 71 +++++++++++++++---- .../src/main/java/envoy/server/data/User.java | 37 +++++++++- 3 files changed, 102 insertions(+), 23 deletions(-) diff --git a/client/src/main/java/envoy/client/ui/controller/LoginScene.java b/client/src/main/java/envoy/client/ui/controller/LoginScene.java index 506170e..71e3988 100644 --- a/client/src/main/java/envoy/client/ui/controller/LoginScene.java +++ b/client/src/main/java/envoy/client/ui/controller/LoginScene.java @@ -1,5 +1,6 @@ package envoy.client.ui.controller; +import java.time.Instant; import java.util.logging.*; import javafx.fxml.FXML; @@ -56,8 +57,8 @@ public final class LoginScene implements EventListener { private boolean registration = false; - private static final Logger logger = EnvoyLog.getLogger(LoginScene.class); - private static final ClientConfig config = ClientConfig.getInstance(); + private static final Logger logger = EnvoyLog.getLogger(LoginScene.class); + private static final ClientConfig config = ClientConfig.getInstance(); @FXML private void initialize() { @@ -74,16 +75,20 @@ public final class LoginScene implements EventListener { @FXML private void loginButtonPressed() { + final String user = userTextField.getText(), pass = passwordField.getText(), repeatPass = repeatPasswordField.getText(); // 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(); 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(); userTextField.clear(); - } else Startup.performHandshake(new LoginCredentials(userTextField.getText(), passwordField.getText(), registration, Startup.VERSION, - Startup.loadLastSync(userTextField.getText()))); + } else { + Instant lastSync = Startup.loadLastSync(userTextField.getText()); + Startup.performHandshake(registration ? LoginCredentials.registration(user, pass, Startup.VERSION, lastSync) + : LoginCredentials.login(user, pass, Startup.VERSION, lastSync)); + } } @FXML diff --git a/common/src/main/java/envoy/data/LoginCredentials.java b/common/src/main/java/envoy/data/LoginCredentials.java index 0bef595..40866ad 100644 --- a/common/src/main/java/envoy/data/LoginCredentials.java +++ b/common/src/main/java/envoy/data/LoginCredentials.java @@ -7,6 +7,9 @@ import java.time.Instant; * Contains a {@link User}'s login / registration information as well as the * client version. *

+ * If the authentication is performed with a token, the token is stored instead + * of the password. + *

* Project: envoy-common
* File: LoginCredentials.java
* Created: 29.12.2019
@@ -17,35 +20,68 @@ import java.time.Instant; public final class LoginCredentials implements Serializable { private final String identifier, password, clientVersion; - private final boolean registration; + private final boolean registration, token; private final Instant lastSync; private static final long serialVersionUID = 3; - /** - * Initializes login credentials for a handshake. - * - * @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) { + private LoginCredentials(String identifier, String password, boolean registration, boolean token, String clientVersion, Instant lastSync) { this.identifier = identifier; this.password = password; this.registration = registration; + this.token = token; this.clientVersion = clientVersion; 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 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, String clientVersion, Instant lastSync) { + return new LoginCredentials(identifier, password, false, false, 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, clientVersion, lastSync); + } + + /** + * Creates login credentials for a registration. + * + * @param identifier the identifier of the user + * @param password the password 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 registration(String identifier, String password, String clientVersion, Instant lastSync) { + return new LoginCredentials(identifier, password, true, false, clientVersion, lastSync); + } + @Override 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,clientVersion=%s,lastSync=%s]", identifier, registration, + token, clientVersion, lastSync); } @@ -69,6 +105,13 @@ public final class LoginCredentials implements Serializable { */ 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 the version of the client sending these credentials * @since Envoy Common v0.1-beta diff --git a/server/src/main/java/envoy/server/data/User.java b/server/src/main/java/envoy/server/data/User.java index 7ff3548..341ebd4 100755 --- a/server/src/main/java/envoy/server/data/User.java +++ b/server/src/main/java/envoy/server/data/User.java @@ -40,7 +40,7 @@ public final class User extends Contact { /** * Named query retrieving a user by name (parameter {@code :name}). - * + * * @since Envoy Server Standalone v0.1-beta */ 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 * {@code :user}). - * + * * @since Envoy Server Standalone v0.1-beta */ 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 * {@code :searchPhrase}) that are not in the contact list of a given user * (parameter {@code :context}). - * + * * @since Envoy Server Standalone v0.1-beta */ public static final String searchByName = "User.searchByName"; @@ -65,6 +65,12 @@ public final class User extends Contact { @Column(name = "password_hash") private String passwordHash; + @Column(name = "auth_token") + private String authToken; + + @Column(name = "auth_token_expiration") + private Instant authTokenExpiration; + @Column(name = "last_seen") private Instant lastSeen; @@ -90,6 +96,31 @@ public final class User extends Contact { */ 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 * @since Envoy Server Standalone v0.2-beta -- 2.30.2 From 31cb22035b5e91baa19d4e251efa9d32172bb18e Mon Sep 17 00:00:00 2001 From: kske Date: Sat, 19 Sep 2020 09:13:04 +0200 Subject: [PATCH 3/6] Add token request to login credentials and "Stay Signed In" checkbox --- .../client/ui/controller/LoginScene.java | 10 ++++-- .../src/main/resources/fxml/LoginScene.fxml | 10 ++++-- .../java/envoy/data/LoginCredentials.java | 31 +++++++++++++------ 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/client/src/main/java/envoy/client/ui/controller/LoginScene.java b/client/src/main/java/envoy/client/ui/controller/LoginScene.java index 71e3988..285f3de 100644 --- a/client/src/main/java/envoy/client/ui/controller/LoginScene.java +++ b/client/src/main/java/envoy/client/ui/controller/LoginScene.java @@ -46,6 +46,9 @@ public final class LoginScene implements EventListener { @FXML private Button loginButton; + @FXML + private CheckBox cbStaySignedIn; + @FXML private Button offlineModeButton; @@ -55,7 +58,7 @@ public final class LoginScene implements EventListener { @FXML private ImageView logo; - private boolean registration = false; + private boolean registration; private static final Logger logger = EnvoyLog.getLogger(LoginScene.class); private static final ClientConfig config = ClientConfig.getInstance(); @@ -76,6 +79,7 @@ public final class LoginScene implements EventListener { @FXML private void loginButtonPressed() { final String user = userTextField.getText(), pass = passwordField.getText(), repeatPass = repeatPasswordField.getText(); + final boolean requestToken = cbStaySignedIn.isSelected(); // Prevent registration with unequal passwords if (registration && !pass.equals(repeatPass)) { @@ -86,8 +90,8 @@ public final class LoginScene implements EventListener { userTextField.clear(); } else { Instant lastSync = Startup.loadLastSync(userTextField.getText()); - Startup.performHandshake(registration ? LoginCredentials.registration(user, pass, Startup.VERSION, lastSync) - : LoginCredentials.login(user, pass, Startup.VERSION, lastSync)); + Startup.performHandshake(registration ? LoginCredentials.registration(user, pass, requestToken, Startup.VERSION, lastSync) + : LoginCredentials.login(user, pass, requestToken, Startup.VERSION, lastSync)); } } diff --git a/client/src/main/resources/fxml/LoginScene.fxml b/client/src/main/resources/fxml/LoginScene.fxml index 1ade0f8..e1316c4 100644 --- a/client/src/main/resources/fxml/LoginScene.fxml +++ b/client/src/main/resources/fxml/LoginScene.fxml @@ -2,6 +2,7 @@ + @@ -32,9 +33,7 @@ -

+ * 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 -- 2.30.2 From 3e594c1fbd39748246d4241b42ba03812ab743f9 Mon Sep 17 00:00:00 2001 From: kske Date: Sat, 19 Sep 2020 13:33:18 +0200 Subject: [PATCH 5/6] Handle handshake rejections on invalid token, reuse not expired tokens --- .../src/main/java/envoy/client/ui/Startup.java | 18 +++++++++++++----- .../processors/LoginCredentialProcessor.java | 16 +++++++++++++--- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/client/src/main/java/envoy/client/ui/Startup.java b/client/src/main/java/envoy/client/ui/Startup.java index c3dea29..0b94d8a 100644 --- a/client/src/main/java/envoy/client/ui/Startup.java +++ b/client/src/main/java/envoy/client/ui/Startup.java @@ -93,8 +93,9 @@ public final class Startup extends Application { 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 + if (!performHandshake( + LoginCredentials.loginWithToken(localDB.getUser().getName(), localDB.getAuthToken(), VERSION, localDB.getLastSync()))) + sceneContext.load(SceneInfo.LOGIN_SCENE); } else { // Load login scene @@ -106,9 +107,10 @@ public final class Startup extends Application { * Tries to perform a Handshake with the server. * * @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 */ - public static void performHandshake(LoginCredentials credentials) { + public static boolean performHandshake(LoginCredentials credentials) { final var cacheMap = new CacheMap(); cacheMap.put(Message.class, new Cache()); cacheMap.put(GroupMessage.class, new Cache()); @@ -120,10 +122,13 @@ public final class Startup extends Application { if (client.isOnline()) { loadChatScene(); client.initReceiver(localDB, cacheMap); + return true; + } else { + return false; } } catch (IOException | InterruptedException | TimeoutException e) { logger.log(Level.INFO, "Could not connect to server. Entering offline mode..."); - attemptOfflineMode(credentials.getIdentifier()); + return attemptOfflineMode(credentials.getIdentifier()); } } @@ -132,9 +137,10 @@ public final class Startup extends Application { * for a given user. * * @param identifier the identifier of the user - currently his username + * @return whether the offline mode could be entered * @since Envoy Client v0.2-beta */ - public static void attemptOfflineMode(String identifier) { + public static boolean attemptOfflineMode(String identifier) { try { // Try entering offline mode localDB.loadUsers(); @@ -142,10 +148,12 @@ public final class Startup extends Application { if (clientUser == null) throw new EnvoyException("Could not enter offline mode: user name unknown"); client.setSender(clientUser); loadChatScene(); + return true; } catch (final Exception e) { new Alert(AlertType.ERROR, "Client error: " + e).showAndWait(); logger.log(Level.SEVERE, "Offline mode could not be loaded: ", e); System.exit(1); + return false; } } diff --git a/server/src/main/java/envoy/server/processors/LoginCredentialProcessor.java b/server/src/main/java/envoy/server/processors/LoginCredentialProcessor.java index 5f5cf01..b14346f 100755 --- a/server/src/main/java/envoy/server/processors/LoginCredentialProcessor.java +++ b/server/src/main/java/envoy/server/processors/LoginCredentialProcessor.java @@ -122,10 +122,20 @@ public final class LoginCredentialProcessor implements ObjectProcessor Date: Sat, 19 Sep 2020 13:43:03 +0200 Subject: [PATCH 6/6] Remove authentication tokens from logs --- common/src/main/java/envoy/event/NewAuthToken.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/src/main/java/envoy/event/NewAuthToken.java b/common/src/main/java/envoy/event/NewAuthToken.java index 9d99c47..81af7b6 100644 --- a/common/src/main/java/envoy/event/NewAuthToken.java +++ b/common/src/main/java/envoy/event/NewAuthToken.java @@ -23,4 +23,7 @@ public class NewAuthToken extends Event { public NewAuthToken(String token) { super(token); } + + @Override + public String toString() { return "NewAuthToken"; } } -- 2.30.2