implemented locking-user-on-login-fail, needs to be tested
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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())));
|
||||||
|
|||||||
Reference in New Issue
Block a user