diff --git a/de.srsoftware.http/src/main/java/de/srsoftware/http/PathHandler.java b/de.srsoftware.http/src/main/java/de/srsoftware/http/PathHandler.java index 796fc65..11b67f8 100644 --- a/de.srsoftware.http/src/main/java/de/srsoftware/http/PathHandler.java +++ b/de.srsoftware.http/src/main/java/de/srsoftware/http/PathHandler.java @@ -11,6 +11,7 @@ import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpsExchange; +import de.srsoftware.utils.Error; import java.io.IOException; import java.util.*; import java.util.stream.Collectors; @@ -176,7 +177,7 @@ public abstract class PathHandler implements HttpHandler { public static boolean sendContent(HttpExchange ex, int status, Object o) throws IOException { if (o instanceof List list) o = new JSONArray(list); if (o instanceof Map map) o = new JSONObject(map); - if (o instanceof JSONObject) ex.getResponseHeaders().add(CONTENT_TYPE, JSON); + if (o instanceof Error error) o = error.json(); return sendContent(ex, status, o.toString().getBytes(UTF_8)); } 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 41f7c77..8d46db7 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 @@ -6,6 +6,7 @@ public class Constants { public static final String ACCESS_TOKEN = "access_token"; public static final String APP_NAME = "LightOIDC"; public static final String AT_HASH = "at_hash"; + public static final String ATTEMPTS = "attempts"; public static final String AUTH_CODE = "authorization_code"; public static final String AUTHORZED = "authorized"; public static final String BEARER = "Bearer"; @@ -41,6 +42,7 @@ public class Constants { public static final String OPENID = "openid"; public static final String REDIRECT_URI = "redirect_uri"; public static final String REDIRECT_URIS = "redirect_uris"; + public static final String RELEASE = "release"; public static final String REQUEST_NOT_SUPPORTED = "request_not_supported"; public static final String RESPONSE_TYPE = "response_type"; public static final String SALT = "salt"; 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 deleted file mode 100644 index 483ba0e..0000000 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Error.java +++ /dev/null @@ -1,36 +0,0 @@ -/* © 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/UserService.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/UserService.java index 47252c2..0ad91a1 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 @@ -6,6 +6,7 @@ import static java.util.Optional.empty; import de.srsoftware.oidc.api.data.AccessToken; import de.srsoftware.oidc.api.data.Lock; import de.srsoftware.oidc.api.data.User; +import de.srsoftware.utils.Result; import java.time.Instant; import java.util.*; 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 c693b66..743bf49 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 @@ -2,6 +2,7 @@ package de.srsoftware.oidc.api.data; import java.time.Instant; +import java.time.temporal.ChronoUnit; public class Lock { private int attempts; @@ -16,7 +17,7 @@ public class Lock { if (attempts > 13) attempts = 13; var seconds = 5; for (long i = 0; i < attempts; i++) seconds *= 2; - releaseTime = Instant.now().plusSeconds(seconds); + releaseTime = Instant.now().plusSeconds(seconds).truncatedTo(ChronoUnit.SECONDS); return this; } 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 b2f565b..52e945a 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 @@ -15,6 +15,8 @@ import de.srsoftware.oidc.api.*; import de.srsoftware.oidc.api.data.Permission; import de.srsoftware.oidc.api.data.Session; import de.srsoftware.oidc.api.data.User; +import de.srsoftware.utils.Payload; +import de.srsoftware.utils.Result; import jakarta.mail.*; import jakarta.mail.internet.*; import java.io.IOException; @@ -193,11 +195,11 @@ public class UserController extends Controller { var username = body.has(USERNAME) ? body.getString(USERNAME) : null; var password = body.has(PASSWORD) ? body.getString(PASSWORD) : null; - var trust = body.has(TRUST) ? body.getBoolean(TRUST) : false; + var trust = body.has(TRUST) && body.getBoolean(TRUST); - Optional user = usersepa.login(username, password); - if (user.isPresent()) return sendUserAndCookie(ex, sessions.createSession(user.get(), trust), user.get()); - return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); + Result result = users.login(username, password); + if (result instanceof Payload user) return sendUserAndCookie(ex, sessions.createSession(user.get(), trust), user.get()); + return sendContent(ex, HTTP_UNAUTHORIZED, result); } private boolean logout(HttpExchange ex, Session session) throws IOException { 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 9ac88df..67c51e1 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 @@ -5,13 +5,13 @@ 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; +import de.srsoftware.utils.Error; import de.srsoftware.utils.PasswordHasher; +import de.srsoftware.utils.Payload; +import de.srsoftware.utils.Result; import java.util.*; public class EncryptedUserService extends EncryptedConfig implements UserService { @@ -103,8 +103,7 @@ public class EncryptedUserService extends EncryptedConfig implements UserService 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()); - Error err = Error.message(ERROR_LOCKED); - return err.metadata("attempts", lock.attempts(), "release", lock.releaseTime()); + return Error.message(ERROR_LOCKED, ATTEMPTS, lock.attempts(), RELEASE, lock.releaseTime()); } for (var encryptedUser : backend.list()) { var decryptedUser = decrypt(encryptedUser); @@ -117,8 +116,7 @@ public class EncryptedUserService extends EncryptedConfig implements UserService var lock = lock(username); 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()); + return Error.message(ERROR_LOGIN_FAILED, 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 2902006..0dcfeb8 100644 --- a/de.srsoftware.oidc.datastore.encrypted/src/test/java/EncryptedUserServiceTest.java +++ b/de.srsoftware.oidc.datastore.encrypted/src/test/java/EncryptedUserServiceTest.java @@ -1,15 +1,17 @@ /* © SRSoftware 2024 */ +import static de.srsoftware.oidc.api.Constants.*; 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.*; 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.Error; import de.srsoftware.utils.PasswordHasher; +import de.srsoftware.utils.Payload; +import de.srsoftware.utils.Result; import java.io.File; import java.util.*; import java.util.stream.Collectors; @@ -74,18 +76,19 @@ public class EncryptedUserServiceTest extends UserServiceTest { if (optLock.isPresent()) { var lock = optLock.get(); LOG.log(WARNING, "{} is locked after {} failed logins. Lock will be released at {}", username, lock.attempts(), lock.releaseTime()); - return Optional.empty(); + return Error.message(ERROR_LOCKED, ATTEMPTS, lock.attempts(), RELEASE, lock.releaseTime()); } for (var entry : users.entrySet()) { var user = entry.getValue(); if (user.username().equals(username) && passwordMatches(password, user)) { unlock(username); - return Optional.of(user); + return Payload.of(user); } } - lock(username); - return Optional.empty(); + var lock = lock(username); + LOG.log(WARNING, "Login failed for {0} → locking account until {1}", username, lock.releaseTime()); + return Error.message(ERROR_LOGIN_FAILED, RELEASE, lock.releaseTime()); } @Override diff --git a/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java b/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java index 559104b..8e2ba9e 100644 --- a/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java +++ b/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java @@ -10,7 +10,10 @@ import static java.util.Optional.empty; import de.srsoftware.oidc.api.*; import de.srsoftware.oidc.api.data.*; +import de.srsoftware.utils.Error; import de.srsoftware.utils.PasswordHasher; +import de.srsoftware.utils.Payload; +import de.srsoftware.utils.Result; import jakarta.mail.Authenticator; import jakarta.mail.PasswordAuthentication; import java.io.File; @@ -176,31 +179,32 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe } @Override - public Optional login(String user, String password) { - if (!json.has(USERS)) return empty(); - var optLock = getLock(user); + public Result login(String username, String password) { + if (!json.has(USERS)) return Error.message(ERROR_LOGIN_FAILED); + 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, "{} is locked after {} failed logins. Lock will be released at {}", user, lock.attempts(), lock.releaseTime()); - return empty(); + LOG.log(WARNING, "{0} is locked after {1} failed logins. Lock will be released at {2}", username, lock.attempts(), lock.releaseTime()); + return Error.message(ERROR_LOCKED, ATTEMPTS, lock.attempts(), RELEASE, lock.releaseTime()); } try { var users = json.getJSONObject(USERS); for (String userId : users.keySet()) { var userData = users.getJSONObject(userId); - if (KEYS.stream().map(userData::getString).noneMatch(val -> val.equals(user))) continue; + if (KEYS.stream().map(userData::getString).noneMatch(val -> val.equals(username))) continue; var loadedUser = User.of(userData, userId).filter(u -> passwordMatches(password, u)); if (loadedUser.isPresent()) { - unlock(user); - return loadedUser; + unlock(username); + return Payload.of(loadedUser.get()); } - lock(userId); } - lock(user); - return empty(); + var lock = lock(username); + LOG.log(WARNING, "Login failed for {0} → locking account until {1}", username, lock.releaseTime()); + return Error.message(ERROR_LOGIN_FAILED, RELEASE, lock.releaseTime()); } catch (Exception e) { - return empty(); + return Error.message(ERROR_LOGIN_FAILED); } } diff --git a/de.srsoftware.oidc.web/src/main/resources/de/login.html b/de.srsoftware.oidc.web/src/main/resources/de/login.html index a920ace..a09defc 100644 --- a/de.srsoftware.oidc.web/src/main/resources/de/login.html +++ b/de.srsoftware.oidc.web/src/main/resources/de/login.html @@ -24,10 +24,16 @@ Passwort - + Fehler Anmeldung fehlgeschlagen! + + Fehler + + Account gesperrt bis + +