Token Based Authentication #30
@ -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,6 +131,20 @@ public final class LocalDB {
|
|||||||
idGenerator = SerializationUtils.read(idGeneratorFile, IDGenerator.class);
|
idGenerator = SerializationUtils.read(idGeneratorFile, IDGenerator.class);
|
||||||
} catch (ClassNotFoundException | IOException e) {}
|
} catch (ClassNotFoundException | IOException e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the last login information. Any exception thrown during this process is
|
||||||
|
* ignored.
|
||||||
|
*
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public void loadLastLogin() {
|
||||||
|
try (var in = new ObjectInputStream(new FileInputStream(lastLoginFile))) {
|
||||||
|
user = (User) in.readObject();
|
||||||
|
authToken = (String) in.readObject();
|
||||||
|
} catch (ClassNotFoundException | IOException e) {}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronizes the contact list of the client user with the chat and user
|
* Synchronizes the contact list of the client user with the chat and user
|
||||||
* storage.
|
* storage.
|
||||||
@ -143,6 +168,11 @@ public final class LocalDB {
|
|||||||
.forEach(chats::add);
|
.forEach(chats::add);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Event
|
||||||
|
private void onNewAuthToken(NewAuthToken evt) {
|
||||||
|
authToken = evt.get();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return a {@code Map<String, User>} of all users stored locally with their
|
* @return a {@code Map<String, User>} of all users stored locally with their
|
||||||
* user names as keys
|
* user names as keys
|
||||||
@ -204,6 +234,12 @@ public final class LocalDB {
|
|||||||
*/
|
*/
|
||||||
public Instant getLastSync() { return lastSync; }
|
public Instant getLastSync() { return lastSync; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the authentication token of the user
|
||||||
|
* @since Envoy Client v0.2-beta
|
||||||
|
*/
|
||||||
|
public String getAuthToken() { return authToken; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Searches for a message by ID.
|
* Searches for a message by ID.
|
||||||
*
|
*
|
||||||
|
@ -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,7 +58,7 @@ 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();
|
||||||
@ -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; }
|
||||||
|
@ -35,6 +35,13 @@ public final class HandshakeRejection extends Event<String> {
|
|||||||
*/
|
*/
|
||||||
public static final String WRONG_VERSION = "Incompatible client version";
|
public static final String WRONG_VERSION = "Incompatible client version";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select this value if the client provided an invalid authentication token.
|
||||||
|
*
|
||||||
|
* @since Envoy Common v0.2-beta
|
||||||
|
*/
|
||||||
|
public static final String INVALID_TOKEN = "Invalid authentication token";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select this value if the handshake could not be completed for some different
|
* Select this value if the handshake could not be completed for some different
|
||||||
* reason.
|
* reason.
|
||||||
|
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(); }
|
||||||
}
|
}
|
||||||
|
@ -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,24 +54,39 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
|
|||||||
try {
|
try {
|
||||||
user = persistenceManager.getUserByName(credentials.getIdentifier());
|
user = persistenceManager.getUserByName(credentials.getIdentifier());
|
||||||
|
|
||||||
// Checking if user is already online
|
// Check if the user is already online
|
||||||
if (connectionManager.isOnline(user.getID())) {
|
if (connectionManager.isOnline(user.getID())) {
|
||||||
logger.warning(user + " is already online!");
|
logger.warning(user + " is already online!");
|
||||||
writeProxy.write(socketID, new HandshakeRejection(INTERNAL_ERROR));
|
writeProxy.write(socketID, new HandshakeRejection(INTERNAL_ERROR));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Evaluating the correctness of the password hash
|
|
||||||
|
// Authenticate with password or token
|
||||||
|
if (credentials.usesToken()) {
|
||||||
|
|
||||||
|
// Check the token
|
||||||
|
if (user.getAuthToken() == null || user.getAuthTokenExpiration().isBefore(Instant.now())
|
||||||
|
|| !user.getAuthToken().equals(credentials.getPassword())) {
|
||||||
|
logger.info(user + " tried to use an invalid token.");
|
||||||
|
writeProxy.write(socketID, new HandshakeRejection(INVALID_TOKEN));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Check the password hash
|
||||||
if (!PasswordUtil.validate(credentials.getPassword(), user.getPasswordHash())) {
|
if (!PasswordUtil.validate(credentials.getPassword(), user.getPasswordHash())) {
|
||||||
logger.info(user + " has entered the wrong password.");
|
logger.info(user + " has entered the wrong password.");
|
||||||
writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER));
|
writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (NoResultException e) {
|
} catch (NoResultException e) {
|
||||||
logger.info("The requested user does not exist.");
|
logger.info("The requested user does not exist.");
|
||||||
writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER));
|
writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// Validate user name
|
// Validate user name
|
||||||
if (!Bounds.isValidContactName(credentials.getIdentifier())) {
|
if (!Bounds.isValidContactName(credentials.getIdentifier())) {
|
||||||
logger.info("The requested user name is not valid.");
|
logger.info("The requested user name is not valid.");
|
||||||
@ -87,7 +94,8 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// Checking that no user already has this identifier
|
|
||||||
|
// Check if the name is taken
|
||||||
PersistenceManager.getInstance().getUserByName(credentials.getIdentifier());
|
PersistenceManager.getInstance().getUserByName(credentials.getIdentifier());
|
||||||
|
|
||||||
// This code only gets executed if this user already exists
|
// This code only gets executed if this user already exists
|
||||||
@ -114,6 +122,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