Browse Source

implemented locking-user-on-login-fail, needs to be tested

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
devel
Stephan Richter 1 month ago
parent
commit
a4200f43aa
  1. 29
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/UserService.java
  2. 32
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/FailedLogin.java
  3. 2
      de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java
  4. 2
      de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java
  5. 9
      de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/WellKnownController.java
  6. 22
      de.srsoftware.oidc.datastore.encrypted/src/main/java/de/srsoftware/oidc/datastore/encrypted/EncryptedUserService.java
  7. 21
      de.srsoftware.oidc.datastore.encrypted/src/test/java/EncryptedUserServiceTest.java
  8. 17
      de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java

29
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/UserService.java

@ -1,13 +1,17 @@
/* © SRSoftware 2024 */ /* © SRSoftware 2024 */
package de.srsoftware.oidc.api; package de.srsoftware.oidc.api;
import static java.util.Optional.empty;
import de.srsoftware.oidc.api.data.AccessToken; import de.srsoftware.oidc.api.data.AccessToken;
import de.srsoftware.oidc.api.data.FailedLogin;
import de.srsoftware.oidc.api.data.User; import de.srsoftware.oidc.api.data.User;
import java.util.List; import java.time.Instant;
import java.util.Optional; import java.util.*;
import java.util.Set;
public interface UserService { public interface UserService {
Map<String, FailedLogin> failedLogins = new HashMap<>();
/** /**
* create a new access token for a given user * create a new access token for a given user
* @param user * @param user
@ -26,9 +30,26 @@ public interface UserService {
public UserService init(User defaultUser); public UserService init(User defaultUser);
public List<User> list(); public List<User> list();
public Set<User> find(String idOrEmail); public Set<User> find(String idOrEmail);
public default Optional<FailedLogin> getLock(String id) {
var failedLogin = failedLogins.get(id);
if (failedLogin == null || failedLogin.releaseTime().isBefore(Instant.now())) return empty();
return Optional.of(failedLogin);
}
public Optional<User> load(String id); public Optional<User> load(String id);
public Optional<User> load(String username, String password); public Optional<User> 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 boolean passwordMatches(String plaintextPassword, User user);
public UserService save(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); public UserService updatePassword(User user, String plaintextPassword);
} }

32
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;
}
}

2
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); HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
var staticPages = (StaticPages) new StaticPages(basePath).bindPath(STATIC_PATH, FAVICON).on(server); var staticPages = (StaticPages) new StaticPages(basePath).bindPath(STATIC_PATH, FAVICON).on(server);
new Forward(INDEX).bindPath(ROOT).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); 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 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); new TokenController(authService, clientService, keyManager, userService, tokenControllerConfig).bindPath(API_TOKEN).on(server);

2
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 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) : false;
Optional<User> user = users.load(username, password); Optional<User> user = users.login(username, password);
if (user.isPresent()) return sendUserAndCookie(ex, sessions.createSession(user.get(), trust), user.get()); if (user.isPresent()) return sendUserAndCookie(ex, sessions.createSession(user.get(), trust), user.get());
return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); return sendEmptyResponse(HTTP_UNAUTHORIZED, ex);
} }

9
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 com.sun.net.httpserver.HttpExchange;
import de.srsoftware.http.PathHandler; import de.srsoftware.http.PathHandler;
import java.io.IOException; import java.io.IOException;
import java.util.List;
import java.util.Map; import java.util.Map;
public class WellKnownController extends PathHandler { public class WellKnownController extends PathHandler {
@ -19,6 +20,12 @@ public class WellKnownController extends PathHandler {
private boolean openidConfig(HttpExchange ex) throws IOException { private boolean openidConfig(HttpExchange ex) throws IOException {
var host = hostname(ex); 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")));
} }
} }

22
de.srsoftware.oidc.datastore.encrypted/src/main/java/de/srsoftware/oidc/datastore/encrypted/EncryptedUserService.java

@ -1,18 +1,17 @@
/* © SRSoftware 2024 */ /* © SRSoftware 2024 */
package de.srsoftware.oidc.datastore.encrypted; package de.srsoftware.oidc.datastore.encrypted;
import static java.lang.System.Logger.Level.WARNING;
import static java.util.Optional.empty; 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.PasswordHasher; import de.srsoftware.utils.PasswordHasher;
import java.util.HashMap; import java.util.*;
import java.util.List;
import java.util.Optional;
import java.util.Set;
public class EncryptedUserService extends EncryptedConfig implements UserService { public class EncryptedUserService extends EncryptedConfig implements UserService {
private static final System.Logger LOG = System.getLogger(EncryptedUserService.class.getSimpleName());
private final UserService backend; private final UserService backend;
private final PasswordHasher hasher; private final PasswordHasher hasher;
@ -94,12 +93,23 @@ public class EncryptedUserService extends EncryptedConfig implements UserService
} }
@Override @Override
public Optional<User> load(String username, String password) { public Optional<User> login(String username, String password) {
if (username == null || username.isBlank()) return empty(); 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()) { for (var encryptedUser : backend.list()) {
var decryptedUser = decrypt(encryptedUser); 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(); return empty();
} }

21
de.srsoftware.oidc.datastore.encrypted/src/test/java/EncryptedUserServiceTest.java

@ -1,6 +1,7 @@
/* © SRSoftware 2024 */ /* © SRSoftware 2024 */
import static de.srsoftware.utils.Optionals.nullable; import static de.srsoftware.utils.Optionals.nullable;
import static de.srsoftware.utils.Strings.uuid; 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.UserService;
import de.srsoftware.oidc.api.UserServiceTest; import de.srsoftware.oidc.api.UserServiceTest;
@ -15,6 +16,7 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
public class EncryptedUserServiceTest extends UserServiceTest { public class EncryptedUserServiceTest extends UserServiceTest {
private static final System.Logger LOG = System.getLogger(EncryptedUserServiceTest.class.getSimpleName());
private class InMemoryUserService implements UserService { private class InMemoryUserService implements UserService {
private final PasswordHasher<String> hasher; private final PasswordHasher<String> hasher;
private HashMap<String, User> users = new HashMap<>(); private HashMap<String, User> users = new HashMap<>();
@ -66,8 +68,23 @@ public class EncryptedUserServiceTest extends UserServiceTest {
} }
@Override @Override
public Optional<User> load(String username, String password) { public Optional<User> login(String username, String password) {
return users.values().stream().filter(user -> user.username().equals(username) && passwordMatches(password, user)).findAny(); 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 @Override

17
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; return this;
} }
@Override @Override
public Set<User> find(String key) { public Set<User> find(String key) {
if (!json.has(USERS)) return Set.of(); if (!json.has(USERS)) return Set.of();
@ -175,8 +176,14 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
} }
@Override @Override
public Optional<User> load(String user, String password) { public Optional<User> login(String user, String password) {
if (!json.has(USERS)) return empty(); 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 { try {
var users = json.getJSONObject(USERS); var users = json.getJSONObject(USERS);
for (String userId : users.keySet()) { 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; if (KEYS.stream().map(userData::getString).noneMatch(val -> val.equals(user))) continue;
var loadedUser = User.of(userData, userId).filter(u -> passwordMatches(password, u)); 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(); return empty();
} catch (Exception e) { } catch (Exception e) {
return empty(); return empty();
@ -210,6 +222,7 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
return save(); return save();
} }
@Override @Override
public FileStore updatePassword(User user, String plaintextPassword) { public FileStore updatePassword(User user, String plaintextPassword) {
return save(user.hashedPassword(passwordHasher.hash(plaintextPassword, uuid()))); return save(user.hashedPassword(passwordHasher.hash(plaintextPassword, uuid())));

Loading…
Cancel
Save