diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Constants.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Constants.java index 5c22717..41f7c77 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Constants.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Constants.java @@ -20,6 +20,9 @@ public class Constants { public static final String DAYS = "days"; public static final String ENCRYPTION_KEY = "encryption_key"; public static final String ERROR_DESCRIPTION = "error_description"; + public static final String ERROR_LOCKED = "error_locked"; + public static final String ERROR_LOGIN_FAILED = "error_login_failed"; + public static final String ERROR_NO_USERNAME = "error_no_username"; public static final String EXPIRATION = "expiration"; public static final String EXPIRES_IN = "expires_in"; public static final String GRANT_TYPE = "grant_type"; diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Error.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Error.java new file mode 100644 index 0000000..483ba0e --- /dev/null +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Error.java @@ -0,0 +1,36 @@ +/* © SRSoftware 2024 */ +package de.srsoftware.oidc.api; + + +import java.util.HashMap; +import java.util.Map; + +public class Error implements Result { + private final String cause; + private Map metadata; + + public Error(String cause) { + this.cause = cause; + } + + public String cause() { + return cause; + } + + @Override + public boolean isError() { + return true; + } + + public static Error message(String text) { + return new Error(text); + } + + public Error metadata(Object... tokens) { + metadata = new HashMap(); + for (int i = 0; i < tokens.length - 1; i += 2) { + metadata.put(tokens[i].toString(), tokens[i + 1]); + } + return this; + } +} diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Payload.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Payload.java new file mode 100644 index 0000000..bcc7e0d --- /dev/null +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Payload.java @@ -0,0 +1,24 @@ +/* © SRSoftware 2024 */ +package de.srsoftware.oidc.api; + + +public class Payload implements Result { + private final T object; + + public Payload(T object) { + this.object = object; + } + + public static Payload of(T object) { + return new Payload<>(object); + } + + @Override + public boolean isError() { + return false; + } + + public T get() { + return object; + } +} diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Result.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Result.java new file mode 100644 index 0000000..281efd3 --- /dev/null +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Result.java @@ -0,0 +1,6 @@ +/* © SRSoftware 2024 */ +package de.srsoftware.oidc.api; + +public interface Result { + public boolean isError(); +} diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/UserService.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/UserService.java index 4331d63..47252c2 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/UserService.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/UserService.java @@ -26,19 +26,19 @@ public interface UserService { * @param accessToken * @return */ - public Optional forToken(String accessToken); - public UserService init(User defaultUser); - public List list(); - public Set find(String idOrEmail); + public Optional forToken(String accessToken); + public UserService init(User defaultUser); + public List list(); + public Set find(String idOrEmail); public default Optional getLock(String key) { var failedLogin = failedLogins.get(key); if (failedLogin == null || failedLogin.releaseTime().isBefore(Instant.now())) return empty(); return Optional.of(failedLogin); } - public Optional load(String id); - public Optional login(String username, String password); - public default Lock lock(String key) { - return failedLogins.computeIfAbsent(key,k -> new Lock()).count(); + public Optional load(String id); + public Result login(String username, String password); + public default Lock lock(String key) { + return failedLogins.computeIfAbsent(key, k -> new Lock()).count(); } public boolean passwordMatches(String plaintextPassword, User user); public UserService save(User user); diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Lock.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Lock.java index 50803ed..c693b66 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Lock.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Lock.java @@ -4,8 +4,8 @@ package de.srsoftware.oidc.api.data; import java.time.Instant; public class Lock { - private int attempts; - private Instant releaseTime; + private int attempts; + private Instant releaseTime; public Lock() { this.attempts = 0; diff --git a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java index 74f5594..b2f565b 100644 --- a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java +++ b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java @@ -195,7 +195,7 @@ public class UserController extends Controller { var password = body.has(PASSWORD) ? body.getString(PASSWORD) : null; var trust = body.has(TRUST) ? body.getBoolean(TRUST) : false; - Optional user = users.login(username, password); + Optional user = usersepa.login(username, password); if (user.isPresent()) return sendUserAndCookie(ex, sessions.createSession(user.get(), trust), user.get()); return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); } diff --git a/de.srsoftware.oidc.datastore.encrypted/src/main/java/de/srsoftware/oidc/datastore/encrypted/EncryptedUserService.java b/de.srsoftware.oidc.datastore.encrypted/src/main/java/de/srsoftware/oidc/datastore/encrypted/EncryptedUserService.java index 85b0997..9ac88df 100644 --- a/de.srsoftware.oidc.datastore.encrypted/src/main/java/de/srsoftware/oidc/datastore/encrypted/EncryptedUserService.java +++ b/de.srsoftware.oidc.datastore.encrypted/src/main/java/de/srsoftware/oidc/datastore/encrypted/EncryptedUserService.java @@ -1,9 +1,13 @@ /* © SRSoftware 2024 */ package de.srsoftware.oidc.datastore.encrypted; +import static de.srsoftware.oidc.api.Constants.*; import static java.lang.System.Logger.Level.WARNING; import static java.util.Optional.empty; +import de.srsoftware.oidc.api.Error; +import de.srsoftware.oidc.api.Payload; +import de.srsoftware.oidc.api.Result; import de.srsoftware.oidc.api.UserService; import de.srsoftware.oidc.api.data.AccessToken; import de.srsoftware.oidc.api.data.User; @@ -93,26 +97,28 @@ public class EncryptedUserService extends EncryptedConfig implements UserService } @Override - public Optional login(String username, String password) { - if (username == null || username.isBlank()) return empty(); + public Result login(String username, String password) { + if (username == null || username.isBlank()) return Error.message(ERROR_NO_USERNAME); var optLock = getLock(username); if (optLock.isPresent()) { var lock = optLock.get(); LOG.log(WARNING, "{0} is locked after {1} failed logins. Lock will be released at {2}", username, lock.attempts(), lock.releaseTime()); - return empty(); + Error err = Error.message(ERROR_LOCKED); + return err.metadata("attempts", lock.attempts(), "release", lock.releaseTime()); } for (var encryptedUser : backend.list()) { var decryptedUser = decrypt(encryptedUser); if (!username.equals(decryptedUser.username())) continue; if (hasher.matches(password, decryptedUser.hashedPassword())) { this.unlock(username); - return Optional.of(decryptedUser); + return Payload.of(decryptedUser); } } var lock = lock(username); - LOG.log(WARNING,"Login failed for {0} → locking account until {1}",username,lock.releaseTime()); - return empty(); + LOG.log(WARNING, "Login failed for {0} → locking account until {1}", username, lock.releaseTime()); + Error err = Error.message(ERROR_LOGIN_FAILED); + return err.metadata("release", lock.releaseTime()); } @Override diff --git a/de.srsoftware.oidc.datastore.encrypted/src/test/java/EncryptedUserServiceTest.java b/de.srsoftware.oidc.datastore.encrypted/src/test/java/EncryptedUserServiceTest.java index c3e33bf..2902006 100644 --- a/de.srsoftware.oidc.datastore.encrypted/src/test/java/EncryptedUserServiceTest.java +++ b/de.srsoftware.oidc.datastore.encrypted/src/test/java/EncryptedUserServiceTest.java @@ -3,6 +3,7 @@ import static de.srsoftware.utils.Optionals.nullable; import static de.srsoftware.utils.Strings.uuid; import static java.lang.System.Logger.Level.WARNING; +import de.srsoftware.oidc.api.Result; import de.srsoftware.oidc.api.UserService; import de.srsoftware.oidc.api.UserServiceTest; import de.srsoftware.oidc.api.data.AccessToken; @@ -68,7 +69,7 @@ public class EncryptedUserServiceTest extends UserServiceTest { } @Override - public Optional login(String username, String password) { + public Result login(String username, String password) { var optLock = getLock(username); if (optLock.isPresent()) { var lock = optLock.get(); diff --git a/de.srsoftware.oidc.web/src/main/resources/en/todo.html b/de.srsoftware.oidc.web/src/main/resources/en/todo.html index 9305055..1473642 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/todo.html +++ b/de.srsoftware.oidc.web/src/main/resources/en/todo.html @@ -13,7 +13,6 @@

to do…

    -
  • implement brute-force countermeasures
  • implement token refresh
  • Configuration im Frontend