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 username, String password);
|
||||
public boolean passwordMatches(String plaintextPassword, User user);
|
||||
public <T extends UserService> T save(User user);
|
||||
public <T extends UserService> T updatePassword(User user, String plaintextPassword);
|
||||
public UserService save(User user);
|
||||
public UserService updatePassword(User user, String plaintextPassword);
|
||||
}
|
||||
|
||||
@@ -96,6 +96,10 @@ public final class User {
|
||||
return Optional.of(user);
|
||||
}
|
||||
|
||||
public Set<Permission> permissions() {
|
||||
return Set.copyOf(permissions);
|
||||
}
|
||||
|
||||
public String realName() {
|
||||
return realName;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import de.srsoftware.oidc.api.*;
|
||||
import de.srsoftware.oidc.api.data.User;
|
||||
import de.srsoftware.oidc.backend.*;
|
||||
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.PlaintextKeyStore;
|
||||
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 {
|
||||
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);
|
||||
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 {
|
||||
|
||||
@@ -15,7 +15,7 @@ dependencies {
|
||||
implementation project(':de.srsoftware.oidc.api')
|
||||
implementation 'com.sun.mail:jakarta.mail:2.0.1'
|
||||
implementation project(':de.srsoftware.utils')
|
||||
|
||||
testImplementation project(path: ':de.srsoftware.oidc.api', configuration: "testBundle")
|
||||
}
|
||||
|
||||
test {
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
/* © SRSoftware 2024 */
|
||||
package de.srsoftware.oidc.datastore.encrypted;
|
||||
|
||||
import static java.util.Optional.empty;
|
||||
|
||||
import de.srsoftware.oidc.api.UserService;
|
||||
import de.srsoftware.oidc.api.data.AccessToken;
|
||||
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.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class EncryptedUserService extends EncryptedConfig implements UserService {
|
||||
private final UserService backend;
|
||||
private final PasswordHasher hasher;
|
||||
|
||||
EncryptedUserService(UserService backend, String key, String salt){
|
||||
super(key,salt);
|
||||
public EncryptedUserService(UserService backend, String key, String salt, PasswordHasher passHasher) {
|
||||
super(key, salt);
|
||||
this.backend = backend;
|
||||
hasher = passHasher;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -28,18 +32,28 @@ public class EncryptedUserService extends EncryptedConfig implements UserService
|
||||
return backend.consumeToken(accessToken).map(this::decrypt);
|
||||
}
|
||||
|
||||
public User decrypt(User secret){
|
||||
return secret;
|
||||
public User decrypt(User 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
|
||||
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;
|
||||
}
|
||||
|
||||
public User encrypt(User plain){
|
||||
return plain;
|
||||
public User encrypt(User 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
|
||||
@@ -60,33 +74,50 @@ return secret;
|
||||
|
||||
@Override
|
||||
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
|
||||
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
|
||||
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
|
||||
public boolean passwordMatches(String plaintextPassword, User user) {
|
||||
return backend.passwordMatches(encrypt(plaintextPassword),encrypt(user));
|
||||
return hasher.matches(plaintextPassword, user.hashedPassword());
|
||||
}
|
||||
|
||||
@Override
|
||||
public EncryptedUserService save(User user) {
|
||||
public UserService save(User user) {
|
||||
delete(user);
|
||||
backend.save(encrypt(user));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EncryptedUserService updatePassword(User user, String plaintextPassword) {
|
||||
backend.updatePassword(encrypt(user),encrypt(plaintextPassword));
|
||||
return this;
|
||||
public UserService updatePassword(User user, String plaintextPassword) {
|
||||
var pass = hasher.hash(plaintextPassword, user.uuid());
|
||||
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