From f5976a7dc367a6027b163bdcfd4264db8f73c35c Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Sat, 5 Oct 2024 09:29:41 +0200 Subject: [PATCH 1/5] added todo Signed-off-by: Stephan Richter --- de.srsoftware.oidc.web/src/main/resources/en/todo.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 bd62295..9305055 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/todo.html +++ b/de.srsoftware.oidc.web/src/main/resources/en/todo.html @@ -13,8 +13,9 @@

to do…

    -
  • implement token refresh
  • -
  • Configuration im Frontend
  • +
  • implement brute-force countermeasures
  • +
  • implement token refresh
  • +
  • Configuration im Frontend
From a4200f43aa91b3537982fd318c84985c3adc5ae8 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Fri, 18 Oct 2024 00:11:40 +0200 Subject: [PATCH 2/5] implemented locking-user-on-login-fail, needs to be tested Signed-off-by: Stephan Richter --- .../de/srsoftware/oidc/api/UserService.java | 45 ++++++++++++++----- .../srsoftware/oidc/api/data/FailedLogin.java | 32 +++++++++++++ .../de/srsoftware/oidc/app/Application.java | 2 +- .../oidc/backend/UserController.java | 2 +- .../oidc/backend/WellKnownController.java | 9 +++- .../encrypted/EncryptedUserService.java | 26 +++++++---- .../test/java/EncryptedUserServiceTest.java | 21 ++++++++- .../oidc/datastore/file/FileStore.java | 17 ++++++- 8 files changed, 127 insertions(+), 27 deletions(-) create mode 100644 de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/FailedLogin.java 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 e9d7822..4ca1996 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 @@ -1,13 +1,17 @@ /* © SRSoftware 2024 */ package de.srsoftware.oidc.api; +import static java.util.Optional.empty; + import de.srsoftware.oidc.api.data.AccessToken; +import de.srsoftware.oidc.api.data.FailedLogin; import de.srsoftware.oidc.api.data.User; -import java.util.List; -import java.util.Optional; -import java.util.Set; +import java.time.Instant; +import java.util.*; public interface UserService { + Map failedLogins = new HashMap<>(); + /** * create a new access token for a given user * @param user @@ -22,13 +26,30 @@ 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 load(String id); - public Optional load(String username, String password); - public boolean passwordMatches(String plaintextPassword, User user); - public UserService save(User user); - public UserService updatePassword(User user, String plaintextPassword); + public Optional forToken(String accessToken); + public UserService init(User defaultUser); + public List list(); + public Set find(String idOrEmail); + public default Optional getLock(String id) { + var failedLogin = failedLogins.get(id); + 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 UserService lock(String id) { + var failedLogin = failedLogins.get(id); + if (failedLogin == null) { + failedLogins.put(id, failedLogin = new FailedLogin(id)); + } + + return this; + } + public boolean passwordMatches(String plaintextPassword, User user); + public UserService save(User user); + public default UserService unlock(String id) { + failedLogins.remove(id); + return this; + } + public UserService updatePassword(User user, String plaintextPassword); } diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/FailedLogin.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/FailedLogin.java new file mode 100644 index 0000000..1550cdd --- /dev/null +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/FailedLogin.java @@ -0,0 +1,32 @@ +/* © SRSoftware 2024 */ +package de.srsoftware.oidc.api.data; + +import java.time.Instant; + +public class FailedLogin { + private final String userId; + private int attempts; + private Instant releaseTime; + + public FailedLogin(String userId) { + this.userId = userId; + this.attempts = 0; + count(); + } + + public void count() { + attempts++; + if (attempts > 13) attempts = 13; + var seconds = 1; + for (long i = 0; i < attempts; i++) seconds *= 2; + releaseTime = Instant.now().plusSeconds(seconds); + } + + public int attempts() { + return attempts; + } + + public Instant releaseTime() { + return releaseTime; + } +} diff --git a/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java b/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java index 84ac47e..8e8e536 100644 --- a/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java +++ b/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java @@ -74,7 +74,7 @@ public class Application { HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); var staticPages = (StaticPages) new StaticPages(basePath).bindPath(STATIC_PATH, FAVICON).on(server); new Forward(INDEX).bindPath(ROOT).on(server); - new WellKnownController().bindPath(WELL_KNOWN).on(server); + new WellKnownController().bindPath(WELL_KNOWN, "/realms/oidc" + WELL_KNOWN).on(server); new UserController(mailConfig, sessionService, userService, staticPages).bindPath(API_USER).on(server); var tokenControllerConfig = new TokenController.Configuration("https://lightoidc.srsoftware.de", 10); // TODO configure or derive from hostname new TokenController(authService, clientService, keyManager, userService, tokenControllerConfig).bindPath(API_TOKEN).on(server); 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 ae0417b..74f5594 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.load(username, password); + Optional user = users.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.backend/src/main/java/de/srsoftware/oidc/backend/WellKnownController.java b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/WellKnownController.java index ce90443..2a0b860 100644 --- a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/WellKnownController.java +++ b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/WellKnownController.java @@ -5,6 +5,7 @@ package de.srsoftware.oidc.backend; import com.sun.net.httpserver.HttpExchange; import de.srsoftware.http.PathHandler; import java.io.IOException; +import java.util.List; import java.util.Map; public class WellKnownController extends PathHandler { @@ -19,6 +20,12 @@ public class WellKnownController extends PathHandler { private boolean openidConfig(HttpExchange ex) throws IOException { var host = hostname(ex); - return sendContent(ex, Map.of("token_endpoint", host + "/api/token", "authorization_endpoint", host + "/web/authorization.html", "userinfo_endpoint", host + "/api/user/info", "jwks_uri", host + "/api/jwks.json", "issuer", "https://lightoidc.srsoftware.de")); + return sendContent(ex, Map.of("token_endpoint", host + "/api/token", // + "authorization_endpoint", host + "/web/authorization.html", // + "userinfo_endpoint", host + "/api/user/info", // + "jwks_uri", host + "/api/jwks.json", // + "issuer", "https://lightoidc.srsoftware.de", // + "id_token_signing_alg_values_supported", List.of("RS256"), // + "subject_types_supported", List.of("public", "pairwise"))); } } 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 bdcd9b8..8a0af78 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,20 +1,19 @@ /* © SRSoftware 2024 */ package de.srsoftware.oidc.datastore.encrypted; +import static java.lang.System.Logger.Level.WARNING; 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.PasswordHasher; -import java.util.HashMap; -import java.util.List; -import java.util.Optional; -import java.util.Set; +import java.util.*; public class EncryptedUserService extends EncryptedConfig implements UserService { - private final UserService backend; - private final PasswordHasher hasher; + private static final System.Logger LOG = System.getLogger(EncryptedUserService.class.getSimpleName()); + private final UserService backend; + private final PasswordHasher hasher; public EncryptedUserService(UserService backend, String key, String salt, PasswordHasher passHasher) { super(key, salt); @@ -94,12 +93,23 @@ public class EncryptedUserService extends EncryptedConfig implements UserService } @Override - public Optional load(String username, String password) { + public Optional login(String username, String password) { if (username == null || username.isBlank()) return empty(); + var optLock = getLock(username); + 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 empty(); + } for (var encryptedUser : backend.list()) { var decryptedUser = decrypt(encryptedUser); - if (username.equals(decryptedUser.username()) && hasher.matches(password, decryptedUser.hashedPassword())) return Optional.of(decryptedUser); + if (!username.equals(decryptedUser.username())) continue; + if (hasher.matches(password, decryptedUser.hashedPassword())) { + this.unlock(username); + return Optional.of(decryptedUser); + } } + lock(username); return empty(); } 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 01fdd02..c3e33bf 100644 --- a/de.srsoftware.oidc.datastore.encrypted/src/test/java/EncryptedUserServiceTest.java +++ b/de.srsoftware.oidc.datastore.encrypted/src/test/java/EncryptedUserServiceTest.java @@ -1,6 +1,7 @@ /* © SRSoftware 2024 */ 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.UserService; import de.srsoftware.oidc.api.UserServiceTest; @@ -15,6 +16,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; public class EncryptedUserServiceTest extends UserServiceTest { + private static final System.Logger LOG = System.getLogger(EncryptedUserServiceTest.class.getSimpleName()); private class InMemoryUserService implements UserService { private final PasswordHasher hasher; private HashMap users = new HashMap<>(); @@ -66,8 +68,23 @@ public class EncryptedUserServiceTest extends UserServiceTest { } @Override - public Optional load(String username, String password) { - return users.values().stream().filter(user -> user.username().equals(username) && passwordMatches(password, user)).findAny(); + public Optional login(String username, String password) { + var optLock = getLock(username); + 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(); + } + + for (var entry : users.entrySet()) { + var user = entry.getValue(); + if (user.username().equals(username) && passwordMatches(password, user)) { + unlock(username); + return Optional.of(user); + } + } + lock(username); + return Optional.empty(); } @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 56d388a..559104b 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 @@ -140,6 +140,7 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe return this; } + @Override public Set find(String key) { if (!json.has(USERS)) return Set.of(); @@ -175,8 +176,14 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe } @Override - public Optional load(String user, String password) { + public Optional login(String user, String password) { if (!json.has(USERS)) return empty(); + var optLock = getLock(user); + 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(); + } try { var users = json.getJSONObject(USERS); for (String userId : users.keySet()) { @@ -184,8 +191,13 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe if (KEYS.stream().map(userData::getString).noneMatch(val -> val.equals(user))) continue; var loadedUser = User.of(userData, userId).filter(u -> passwordMatches(password, u)); - if (loadedUser.isPresent()) return loadedUser; + if (loadedUser.isPresent()) { + unlock(user); + return loadedUser; + } + lock(userId); } + lock(user); return empty(); } catch (Exception e) { return empty(); @@ -210,6 +222,7 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe return save(); } + @Override public FileStore updatePassword(User user, String plaintextPassword) { return save(user.hashedPassword(passwordHasher.hash(plaintextPassword, uuid()))); From a10224a23ec61ad25bf8bfc8a629acadd0c63298 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Fri, 18 Oct 2024 13:57:43 +0200 Subject: [PATCH 3/5] implemented brute force protection Signed-off-by: Stephan Richter --- .../de/srsoftware/oidc/api/UserService.java | 21 +++++++------------ .../api/data/{FailedLogin.java => Lock.java} | 12 +++++------ .../encrypted/EncryptedUserService.java | 6 ++++-- 3 files changed, 17 insertions(+), 22 deletions(-) rename de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/{FailedLogin.java => Lock.java} (71%) 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 4ca1996..4331d63 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 @@ -4,13 +4,13 @@ package de.srsoftware.oidc.api; import static java.util.Optional.empty; import de.srsoftware.oidc.api.data.AccessToken; -import de.srsoftware.oidc.api.data.FailedLogin; +import de.srsoftware.oidc.api.data.Lock; import de.srsoftware.oidc.api.data.User; import java.time.Instant; import java.util.*; public interface UserService { - Map failedLogins = new HashMap<>(); + Map failedLogins = new HashMap<>(); /** * create a new access token for a given user @@ -30,25 +30,20 @@ public interface UserService { public UserService init(User defaultUser); public List list(); public Set find(String idOrEmail); - public default Optional getLock(String id) { - var failedLogin = failedLogins.get(id); + 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 UserService lock(String id) { - var failedLogin = failedLogins.get(id); - if (failedLogin == null) { - failedLogins.put(id, failedLogin = new FailedLogin(id)); - } - - return this; + 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); - public default UserService unlock(String id) { - failedLogins.remove(id); + public default UserService unlock(String key) { + failedLogins.remove(key); return this; } public UserService updatePassword(User user, String plaintextPassword); diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/FailedLogin.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Lock.java similarity index 71% rename from de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/FailedLogin.java rename to de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Lock.java index 1550cdd..50803ed 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/FailedLogin.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Lock.java @@ -3,23 +3,21 @@ package de.srsoftware.oidc.api.data; import java.time.Instant; -public class FailedLogin { - private final String userId; +public class Lock { private int attempts; private Instant releaseTime; - public FailedLogin(String userId) { - this.userId = userId; + public Lock() { this.attempts = 0; - count(); } - public void count() { + public Lock count() { attempts++; if (attempts > 13) attempts = 13; - var seconds = 1; + var seconds = 5; for (long i = 0; i < attempts; i++) seconds *= 2; releaseTime = Instant.now().plusSeconds(seconds); + return this; } public int attempts() { 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 8a0af78..85b0997 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 @@ -98,7 +98,7 @@ public class EncryptedUserService extends EncryptedConfig implements UserService var optLock = getLock(username); 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()); + LOG.log(WARNING, "{0} is locked after {1} failed logins. Lock will be released at {2}", username, lock.attempts(), lock.releaseTime()); return empty(); } for (var encryptedUser : backend.list()) { @@ -109,7 +109,9 @@ public class EncryptedUserService extends EncryptedConfig implements UserService return Optional.of(decryptedUser); } } - lock(username); + + var lock = lock(username); + LOG.log(WARNING,"Login failed for {0} → locking account until {1}",username,lock.releaseTime()); return empty(); } From 951c65c12141822fe3fed8284cecb1bff575041b Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Fri, 18 Oct 2024 19:35:40 +0200 Subject: [PATCH 4/5] preparing to pass error messages to client Signed-off-by: Stephan Richter --- .../de/srsoftware/oidc/api/Constants.java | 3 ++ .../java/de/srsoftware/oidc/api/Error.java | 36 +++++++++++++++++++ .../java/de/srsoftware/oidc/api/Payload.java | 24 +++++++++++++ .../java/de/srsoftware/oidc/api/Result.java | 6 ++++ .../de/srsoftware/oidc/api/UserService.java | 16 ++++----- .../de/srsoftware/oidc/api/data/Lock.java | 4 +-- .../oidc/backend/UserController.java | 2 +- .../encrypted/EncryptedUserService.java | 18 ++++++---- .../test/java/EncryptedUserServiceTest.java | 3 +- .../src/main/resources/en/todo.html | 1 - 10 files changed, 94 insertions(+), 19 deletions(-) create mode 100644 de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Error.java create mode 100644 de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Payload.java create mode 100644 de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Result.java 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
From 5458e6d015fd1e127aaedbaa49f5bc167e7814cf Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Fri, 18 Oct 2024 21:04:00 +0200 Subject: [PATCH 5/5] improved error message display on login papge Signed-off-by: Stephan Richter --- .../java/de/srsoftware/http/PathHandler.java | 3 +- .../de/srsoftware/oidc/api/Constants.java | 2 + .../java/de/srsoftware/oidc/api/Error.java | 36 ----------------- .../de/srsoftware/oidc/api/UserService.java | 1 + .../de/srsoftware/oidc/api/data/Lock.java | 3 +- .../oidc/backend/UserController.java | 10 +++-- .../encrypted/EncryptedUserService.java | 12 +++--- .../test/java/EncryptedUserServiceTest.java | 17 ++++---- .../oidc/datastore/file/FileStore.java | 28 +++++++------ .../src/main/resources/de/login.html | 12 ++++-- .../src/main/resources/en/login.html | 8 +++- .../src/main/resources/en/scripts/login.js | 11 ++--- de.srsoftware.utils/build.gradle | 1 + .../main/java/de/srsoftware/utils/Error.java | 40 +++++++++++++++++++ .../java/de/srsoftware/utils}/Payload.java | 2 +- .../java/de/srsoftware/utils}/Result.java | 2 +- 16 files changed, 109 insertions(+), 79 deletions(-) delete mode 100644 de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Error.java create mode 100644 de.srsoftware.utils/src/main/java/de/srsoftware/utils/Error.java rename {de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api => de.srsoftware.utils/src/main/java/de/srsoftware/utils}/Payload.java (91%) rename {de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api => de.srsoftware.utils/src/main/java/de/srsoftware/utils}/Result.java (72%) 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 + +
diff --git a/de.srsoftware.oidc.web/src/main/resources/en/login.html b/de.srsoftware.oidc.web/src/main/resources/en/login.html index 3483b3d..219b87a 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/login.html +++ b/de.srsoftware.oidc.web/src/main/resources/en/login.html @@ -24,10 +24,16 @@ Password - + Error Failed to log in! + + Error + + Your account is locked until + +