implemented EncryptedUserService
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -29,6 +29,6 @@ public interface UserService {
|
|||||||
public Optional<User> load(String id);
|
public Optional<User> load(String id);
|
||||||
public Optional<User> load(String username, String password);
|
public Optional<User> load(String username, String password);
|
||||||
public boolean passwordMatches(String plaintextPassword, User user);
|
public boolean passwordMatches(String plaintextPassword, User user);
|
||||||
public <T extends UserService> T save(User user);
|
public UserService save(User user);
|
||||||
public <T extends UserService> T updatePassword(User user, String plaintextPassword);
|
public UserService updatePassword(User user, String plaintextPassword);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,10 @@ public final class User {
|
|||||||
return Optional.of(user);
|
return Optional.of(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Set<Permission> permissions() {
|
||||||
|
return Set.copyOf(permissions);
|
||||||
|
}
|
||||||
|
|
||||||
public String realName() {
|
public String realName() {
|
||||||
return realName;
|
return realName;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import de.srsoftware.oidc.api.*;
|
|||||||
import de.srsoftware.oidc.api.data.User;
|
import de.srsoftware.oidc.api.data.User;
|
||||||
import de.srsoftware.oidc.backend.*;
|
import de.srsoftware.oidc.backend.*;
|
||||||
import de.srsoftware.oidc.datastore.encrypted.EncryptedMailConfig;
|
import de.srsoftware.oidc.datastore.encrypted.EncryptedMailConfig;
|
||||||
|
import de.srsoftware.oidc.datastore.encrypted.EncryptedUserService;
|
||||||
import de.srsoftware.oidc.datastore.file.FileStoreProvider;
|
import de.srsoftware.oidc.datastore.file.FileStoreProvider;
|
||||||
import de.srsoftware.oidc.datastore.file.PlaintextKeyStore;
|
import de.srsoftware.oidc.datastore.file.PlaintextKeyStore;
|
||||||
import de.srsoftware.oidc.datastore.sqlite.*;
|
import de.srsoftware.oidc.datastore.sqlite.*;
|
||||||
@@ -128,10 +129,18 @@ public class Application {
|
|||||||
|
|
||||||
private static UserService setupUserService(Configuration config, Path defaultFile, FileStoreProvider fileStoreProvider, UuidHasher passHasher) throws SQLException {
|
private static UserService setupUserService(Configuration config, Path defaultFile, FileStoreProvider fileStoreProvider, UuidHasher passHasher) throws SQLException {
|
||||||
var userStorageLocation = new File(config.getOrDefault("user_storage",defaultFile));
|
var userStorageLocation = new File(config.getOrDefault("user_storage",defaultFile));
|
||||||
return switch (extension(userStorageLocation).toLowerCase()){
|
var userService = switch (extension(userStorageLocation).toLowerCase()){
|
||||||
case "db", "sqlite", "sqlite3" -> new SqliteUserService(connectionProvider.get(userStorageLocation),passHasher);
|
case "db", "sqlite", "sqlite3" -> new SqliteUserService(connectionProvider.get(userStorageLocation),passHasher);
|
||||||
default -> fileStoreProvider.get(userStorageLocation);
|
default -> fileStoreProvider.get(userStorageLocation);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Optional<String> encryptionKey = config.get(ENCRYPTION_KEY);
|
||||||
|
|
||||||
|
if (encryptionKey.isPresent()){
|
||||||
|
var salt = config.getOrDefault(SALT,uuid());
|
||||||
|
userService = new EncryptedUserService(userService,encryptionKey.get(),salt,passHasher);
|
||||||
|
}
|
||||||
|
return userService;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static KeyStorage setupKeyStore(Configuration config, Path defaultConfigDir) throws SQLException {
|
private static KeyStorage setupKeyStore(Configuration config, Path defaultConfigDir) throws SQLException {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ dependencies {
|
|||||||
implementation project(':de.srsoftware.oidc.api')
|
implementation project(':de.srsoftware.oidc.api')
|
||||||
implementation 'com.sun.mail:jakarta.mail:2.0.1'
|
implementation 'com.sun.mail:jakarta.mail:2.0.1'
|
||||||
implementation project(':de.srsoftware.utils')
|
implementation project(':de.srsoftware.utils')
|
||||||
|
testImplementation project(path: ':de.srsoftware.oidc.api', configuration: "testBundle")
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
|
/* © SRSoftware 2024 */
|
||||||
package de.srsoftware.oidc.datastore.encrypted;
|
package de.srsoftware.oidc.datastore.encrypted;
|
||||||
|
|
||||||
|
import static java.util.Optional.empty;
|
||||||
|
|
||||||
import de.srsoftware.oidc.api.UserService;
|
import de.srsoftware.oidc.api.UserService;
|
||||||
import de.srsoftware.oidc.api.data.AccessToken;
|
import de.srsoftware.oidc.api.data.AccessToken;
|
||||||
import de.srsoftware.oidc.api.data.User;
|
import de.srsoftware.oidc.api.data.User;
|
||||||
import de.srsoftware.utils.Optionals;
|
import de.srsoftware.utils.PasswordHasher;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class EncryptedUserService extends EncryptedConfig implements UserService {
|
public class EncryptedUserService extends EncryptedConfig implements UserService {
|
||||||
private final UserService backend;
|
private final UserService backend;
|
||||||
|
private final PasswordHasher hasher;
|
||||||
|
|
||||||
EncryptedUserService(UserService backend, String key, String salt){
|
public EncryptedUserService(UserService backend, String key, String salt, PasswordHasher passHasher) {
|
||||||
super(key, salt);
|
super(key, salt);
|
||||||
this.backend = backend;
|
this.backend = backend;
|
||||||
|
hasher = passHasher;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -29,17 +33,27 @@ public class EncryptedUserService extends EncryptedConfig implements UserService
|
|||||||
}
|
}
|
||||||
|
|
||||||
public User decrypt(User secret) {
|
public User decrypt(User secret) {
|
||||||
return secret;
|
var decrypted = new User(decrypt(secret.username()), decrypt(secret.hashedPassword()), decrypt(secret.realName()), decrypt(secret.email()), decrypt(secret.uuid())).sessionDuration(secret.sessionDuration());
|
||||||
|
secret.permissions().forEach(decrypted::add);
|
||||||
|
return decrypted;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserService delete(User user) {
|
public UserService delete(User user) {
|
||||||
backend.delete(encrypt(user));
|
for (var encryptedUser : backend.list()) {
|
||||||
|
var decryptedUser = decrypt(encryptedUser);
|
||||||
|
if (decryptedUser.uuid().equals(user.uuid())) {
|
||||||
|
backend.delete(encryptedUser);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public User encrypt(User plain) {
|
public User encrypt(User plain) {
|
||||||
return plain;
|
var encrypted = new User(encrypt(plain.username()), encrypt(plain.hashedPassword()), encrypt(plain.realName()), encrypt(plain.email()), encrypt(plain.uuid())).sessionDuration(plain.sessionDuration());
|
||||||
|
plain.permissions().forEach(encrypted::add);
|
||||||
|
return encrypted;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -60,33 +74,50 @@ return secret;
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<User> find(String idOrEmail) {
|
public Set<User> find(String idOrEmail) {
|
||||||
return backend.find(idOrEmail).stream().map(this::decrypt).collect(Collectors.toSet());
|
if (idOrEmail == null || idOrEmail.isBlank()) return Set.of();
|
||||||
|
var matching = new HashMap<String, User>();
|
||||||
|
for (var encryptedUser : backend.list()) {
|
||||||
|
var decryptedUser = decrypt(encryptedUser);
|
||||||
|
if (idOrEmail.equals(decryptedUser.uuid()) || idOrEmail.equals(decryptedUser.email()) || idOrEmail.equals(decryptedUser.username()) || decryptedUser.realName().contains(idOrEmail)) matching.put(decryptedUser.uuid(), decryptedUser);
|
||||||
|
}
|
||||||
|
return Set.copyOf(matching.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<User> load(String id) {
|
public Optional<User> load(String id) {
|
||||||
return backend.load(id).map(this::decrypt);
|
if (id == null || id.isBlank()) return empty();
|
||||||
|
for (var encryptedUser : backend.list()) {
|
||||||
|
var decryptedUser = decrypt(encryptedUser);
|
||||||
|
if (id.equals(decryptedUser.uuid())) return Optional.of(decryptedUser);
|
||||||
|
}
|
||||||
|
return empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<User> load(String username, String password) {
|
public Optional<User> load(String username, String password) {
|
||||||
return backend.load(encrypt(username),encrypt(password));
|
if (username == null || username.isBlank()) return empty();
|
||||||
|
for (var encryptedUser : backend.list()) {
|
||||||
|
var decryptedUser = decrypt(encryptedUser);
|
||||||
|
if (username.equals(decryptedUser.username()) && hasher.matches(password, decryptedUser.hashedPassword())) return Optional.of(decryptedUser);
|
||||||
|
}
|
||||||
|
return empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean passwordMatches(String plaintextPassword, User user) {
|
public boolean passwordMatches(String plaintextPassword, User user) {
|
||||||
return backend.passwordMatches(encrypt(plaintextPassword),encrypt(user));
|
return hasher.matches(plaintextPassword, user.hashedPassword());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public EncryptedUserService save(User user) {
|
public UserService save(User user) {
|
||||||
|
delete(user);
|
||||||
backend.save(encrypt(user));
|
backend.save(encrypt(user));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public EncryptedUserService updatePassword(User user, String plaintextPassword) {
|
public UserService updatePassword(User user, String plaintextPassword) {
|
||||||
backend.updatePassword(encrypt(user),encrypt(plaintextPassword));
|
var pass = hasher.hash(plaintextPassword, user.uuid());
|
||||||
return this;
|
return save(user.hashedPassword(pass));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
/* © SRSoftware 2024 */
|
||||||
|
import static de.srsoftware.utils.Optionals.nullable;
|
||||||
|
import static de.srsoftware.utils.Strings.uuid;
|
||||||
|
|
||||||
|
import de.srsoftware.oidc.api.UserService;
|
||||||
|
import de.srsoftware.oidc.api.UserServiceTest;
|
||||||
|
import de.srsoftware.oidc.api.data.AccessToken;
|
||||||
|
import de.srsoftware.oidc.api.data.User;
|
||||||
|
import de.srsoftware.oidc.datastore.encrypted.EncryptedUserService;
|
||||||
|
import de.srsoftware.utils.PasswordHasher;
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
|
||||||
|
public class EncryptedUserServiceTest extends UserServiceTest {
|
||||||
|
private class InMemoryUserService implements UserService {
|
||||||
|
private final PasswordHasher<String> hasher;
|
||||||
|
private HashMap<String, User> users = new HashMap<>();
|
||||||
|
|
||||||
|
public InMemoryUserService(PasswordHasher<String> hasher) {
|
||||||
|
this.hasher = hasher;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccessToken accessToken(User user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<User> consumeToken(String accessToken) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserService delete(User user) {
|
||||||
|
users.remove(user.uuid());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<User> forToken(String accessToken) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserService init(User defaultUser) {
|
||||||
|
if (users.isEmpty()) users.put(defaultUser.uuid(), defaultUser);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<User> list() {
|
||||||
|
return List.copyOf(users.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<User> find(String idOrEmail) {
|
||||||
|
return list().stream().filter(user -> user.uuid().equals(idOrEmail) || user.email().equals(idOrEmail)).collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<User> load(String id) {
|
||||||
|
return nullable(users.get(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<User> load(String username, String password) {
|
||||||
|
return users.values().stream().filter(user -> user.username().equals(username) && passwordMatches(password, user)).findAny();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean passwordMatches(String plaintextPassword, User user) {
|
||||||
|
return hasher.matches(plaintextPassword, user.hashedPassword());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserService save(User user) {
|
||||||
|
users.put(user.uuid(), user);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserService updatePassword(User user, String plaintextPassword) {
|
||||||
|
var old = users.get(user.uuid());
|
||||||
|
save(user.hashedPassword(hasher.hash(plaintextPassword, uuid())));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private File storage = new File("/tmp/" + UUID.randomUUID());
|
||||||
|
private UserService userService;
|
||||||
|
private String key, salt;
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
public void tearDown() {
|
||||||
|
if (storage.exists()) storage.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setup() {
|
||||||
|
tearDown();
|
||||||
|
key = uuid();
|
||||||
|
salt = uuid();
|
||||||
|
InMemoryUserService backend = new InMemoryUserService(hasher());
|
||||||
|
userService = new EncryptedUserService(backend, key, salt, hasher());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected UserService userService() {
|
||||||
|
return userService;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user