implemented brute force counter measure
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.core;
|
||||
|
||||
import static java.lang.System.Logger.Level.INFO;
|
||||
|
||||
import java.io.File;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
@@ -8,7 +10,7 @@ import java.util.HashMap;
|
||||
import org.sqlite.SQLiteDataSource;
|
||||
|
||||
public class ConnectionProvider {
|
||||
|
||||
private static final System.Logger LOG = System.getLogger(ConnectionProvider.class.getSimpleName());
|
||||
private static final HashMap<File, Connection> connections = new HashMap<>();
|
||||
|
||||
private ConnectionProvider(){}
|
||||
@@ -17,7 +19,10 @@ public class ConnectionProvider {
|
||||
if (o instanceof String filename) o = new File(filename);
|
||||
if (o instanceof File dbFile) try {
|
||||
var conn = connections.get(dbFile);
|
||||
if (conn == null) connections.put(dbFile, conn = open(dbFile));
|
||||
if (conn == null) {
|
||||
connections.put(dbFile, conn = open(dbFile));
|
||||
LOG.log(INFO,"Using {0}",dbFile);
|
||||
}
|
||||
return conn;
|
||||
} catch (SQLException sqle) {
|
||||
throw new RuntimeException(sqle);
|
||||
|
||||
@@ -9,11 +9,16 @@
|
||||
let credentials = $state({ username : null, password : null });
|
||||
const router = useTinyRouter();
|
||||
let services = $state([]);
|
||||
let error = $state(null);
|
||||
|
||||
|
||||
function doLogin(ev){
|
||||
async function doLogin(ev){
|
||||
ev.preventDefault();
|
||||
tryLogin(credentials);
|
||||
const json = await tryLogin(credentials);
|
||||
if (json) {
|
||||
json.release_time = json.release_time.replace('T',' ');
|
||||
error = t('failed_login_attempts',json);
|
||||
}
|
||||
}
|
||||
|
||||
function init(element){
|
||||
@@ -76,6 +81,9 @@
|
||||
|
||||
<form onsubmit={doLogin}>
|
||||
<fieldset>
|
||||
{#if error}
|
||||
<span class="error">{error}</span>
|
||||
{/if}
|
||||
<legend>{t('login')}</legend>
|
||||
<label>
|
||||
<input type="text" bind:value={credentials.username} required use:init />
|
||||
|
||||
@@ -43,8 +43,10 @@ export async function tryLogin(credentials){
|
||||
if (response.ok){
|
||||
const json = await response.json();
|
||||
for (let key of Object.keys(json)) user[key] = json[key];
|
||||
|
||||
return null;
|
||||
} else {
|
||||
alert("Login failed!");
|
||||
let json = await response.json();
|
||||
return json;
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,7 @@
|
||||
"extended_settings": "erweiterte Einstellungen",
|
||||
|
||||
"failed": "fehlgeschlagen",
|
||||
"failed_login_attempts" : "Account nach {attempts} fehlgeschlagenen Logins gesperrt bis {release_time}",
|
||||
"files": "Dateien",
|
||||
"filter": "Filter",
|
||||
"footer": "Fuß-Text",
|
||||
|
||||
@@ -16,22 +16,21 @@ public class Constants {
|
||||
public static final String DEFAULT_FIELD = "sub";
|
||||
public static final Duration DEFAULT_SESSION_DURATION = Duration.ofMinutes(20);
|
||||
|
||||
public static final String FAILED_LOGINS = "failed_logins";
|
||||
public static final String FOREIGN_ID = "foreign_id";
|
||||
public static final String GRANT_TYPE = "grant_type";
|
||||
|
||||
public static final String IDS = "ids";
|
||||
public static final String ID_TOKEN = "id_token";
|
||||
public static final String JWKS_ENDPOINT = "jwks_uri";
|
||||
|
||||
public static final String LAST_LOGOFF = "last_logoff";
|
||||
public static final String LOGIN_SERVICES = "login_services";
|
||||
public static final String MESSAGE_DELIVERY = "message_delivery";
|
||||
public static final String OIDC = "oidc";
|
||||
public static final String OIDC_CALLBACK = "redirect_uri";
|
||||
public static final String OIDC_LINK = "oidc_link";
|
||||
public static final String OIDC_SCOPE = "openid";
|
||||
|
||||
public static final String REDIRECT_URI = "redirect_uri";
|
||||
public static final String RELEASE_TIME = "release_time";
|
||||
public static final String RESPONSE_TYPE = "response_type";
|
||||
public static final String SCOPE = "scope";
|
||||
public static final String SERVICE_ID = "service_id";
|
||||
|
||||
@@ -513,14 +513,23 @@ public class UserModule extends BaseHandler implements UserService {
|
||||
private boolean postLogin(HttpExchange ex) throws IOException {
|
||||
var json = json(ex);
|
||||
if (!(json.has(USERNAME) && json.get(USERNAME) instanceof String username)) return sendContent(ex, HTTP_UNPROCESSABLE,"Username missing");
|
||||
var optLock = Lock.get(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 sendContent(ex,HTTP_UNAUTHORIZED,lock);
|
||||
}
|
||||
|
||||
if (!(json.has(PASSWORD) && json.get(PASSWORD) instanceof String password)) return sendContent(ex, HTTP_UNPROCESSABLE,"Password missing");
|
||||
if (password.isBlank()) return sendContent(ex,HTTP_UNAUTHORIZED,"Password must not be blank");
|
||||
var hashedPass = Password.of(BAD_HASHER.hash(password,null));
|
||||
try {
|
||||
var user = users.load(username, hashedPass);
|
||||
Lock.unlock(username);
|
||||
users.getSession(user).cookie().addTo(ex);
|
||||
return sendContent(ex,user);
|
||||
} catch (UmbrellaException e){
|
||||
if (e.statusCode() == HTTP_UNAUTHORIZED) return sendContent(ex,HTTP_UNAUTHORIZED,Lock.lock(username));
|
||||
return send(ex,e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.user.model;
|
||||
|
||||
import static java.time.temporal.ChronoUnit.SECONDS;
|
||||
import static java.util.Optional.empty;
|
||||
|
||||
import de.srsoftware.tools.Mappable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public class Lock implements Mappable {
|
||||
private static final Map<String, Lock> locks = new HashMap<>();
|
||||
|
||||
private int attempts;
|
||||
private LocalDateTime releaseTime;
|
||||
|
||||
public Lock() {
|
||||
this.attempts = 0;
|
||||
}
|
||||
|
||||
|
||||
public int attempts() {
|
||||
return attempts;
|
||||
}
|
||||
|
||||
public Lock count() {
|
||||
attempts++;
|
||||
if (attempts > 13) attempts = 13;
|
||||
var seconds = 5;
|
||||
for (long i = 0; i < attempts; i++) seconds *= 2;
|
||||
releaseTime = LocalDateTime.now().plusSeconds(seconds).truncatedTo(SECONDS);
|
||||
return this;
|
||||
}
|
||||
|
||||
public static Optional<Lock> get(String key) {
|
||||
var failedLogin = locks.get(key);
|
||||
if (failedLogin == null || failedLogin.releaseTime().isBefore(LocalDateTime.now())) return empty();
|
||||
return Optional.of(failedLogin);
|
||||
}
|
||||
|
||||
public static Lock lock(String key) {
|
||||
return locks.computeIfAbsent(key, k -> new Lock()).count();
|
||||
}
|
||||
|
||||
|
||||
public LocalDateTime releaseTime() {
|
||||
return releaseTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> toMap() {
|
||||
return Map.of("attempts",attempts,"release_time",releaseTime);
|
||||
}
|
||||
|
||||
public static void unlock(String key) {
|
||||
locks.remove(key);
|
||||
}
|
||||
}
|
||||
@@ -8,15 +8,15 @@ import static de.srsoftware.umbrella.core.Constants.*;
|
||||
import static de.srsoftware.umbrella.user.Constants.*;
|
||||
import static de.srsoftware.umbrella.user.model.DbUser.ADMIN_PERMISSIONS;
|
||||
import static java.lang.System.Logger.Level.*;
|
||||
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
|
||||
import static java.text.MessageFormat.format;
|
||||
import static java.time.ZoneOffset.UTC;
|
||||
|
||||
import de.srsoftware.tools.PasswordHasher;
|
||||
import de.srsoftware.tools.jdbc.Query;
|
||||
import de.srsoftware.umbrella.core.BaseDb;
|
||||
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
|
||||
import de.srsoftware.umbrella.core.model.EmailAddress;
|
||||
import de.srsoftware.umbrella.core.model.Session;
|
||||
import de.srsoftware.umbrella.core.model.Token;
|
||||
import de.srsoftware.umbrella.core.model.UmbrellaUser;
|
||||
import de.srsoftware.umbrella.core.model.*;
|
||||
import de.srsoftware.umbrella.user.BadHasher;
|
||||
import de.srsoftware.umbrella.user.api.LoginServiceDb;
|
||||
import de.srsoftware.umbrella.user.api.UserDb;
|
||||
@@ -29,20 +29,15 @@ import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.*;
|
||||
|
||||
public class SqliteDB implements LoginServiceDb, UserDb {
|
||||
public class SqliteDB extends BaseDb implements LoginServiceDb, UserDb {
|
||||
private static final System.Logger LOG = System.getLogger(SqliteDB.class.getSimpleName());
|
||||
|
||||
private static final int INITIAL_DB_VERSION = 4;
|
||||
|
||||
private HashMap<String,Session> sessions = new HashMap<>();
|
||||
private final Connection db;
|
||||
|
||||
public SqliteDB(Connection conn){
|
||||
db = conn;
|
||||
init();
|
||||
super(conn);
|
||||
}
|
||||
|
||||
|
||||
@@ -94,43 +89,21 @@ CREATE TABLE IF NOT EXISTS {0} (
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private int createSettingsTable() {
|
||||
var createTable = """
|
||||
CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255) NOT NULL);
|
||||
""";
|
||||
try {
|
||||
var stmt = db.prepareStatement(format(createTable,TABLE_SETTINGS, KEY, VALUE));
|
||||
stmt.execute();
|
||||
stmt.close();
|
||||
} catch (SQLException e) {
|
||||
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_SETTINGS,e);
|
||||
throw new RuntimeException(e);
|
||||
protected int createTables() {
|
||||
int currentVersion = createSettingsTable();
|
||||
switch (currentVersion){
|
||||
case 0:
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
createUserTables();
|
||||
createLoginServiceTables();
|
||||
}
|
||||
|
||||
Integer version = null;
|
||||
try {
|
||||
var rs = Query.select(VALUE).from(TABLE_SETTINGS).where(KEY, equal(DB_VERSION)).exec(db);
|
||||
if (rs.next()) version = rs.getInt(VALUE);
|
||||
rs.close();
|
||||
if (version == null) {
|
||||
version = INITIAL_DB_VERSION;
|
||||
insertInto(TABLE_SETTINGS, KEY, VALUE).values(DB_VERSION,version).execute(db).close();
|
||||
}
|
||||
return setCurrentVersion(4);
|
||||
|
||||
return version;
|
||||
} catch (SQLException e) {
|
||||
LOG.log(ERROR,ERROR_READ_TABLE,DB_VERSION,TABLE_SETTINGS,e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private int createTables() {
|
||||
createUserTables();
|
||||
createLoginServiceTables();
|
||||
return createSettingsTable();
|
||||
}
|
||||
|
||||
private void createUserTables() {
|
||||
PasswordHasher<String> hasher;
|
||||
@@ -287,13 +260,6 @@ CREATE TABLE IF NOT EXISTS {0} (
|
||||
return userId;
|
||||
}
|
||||
|
||||
private void init(){
|
||||
var version = createTables();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public Map<Long, UmbrellaUser> list(Integer start, Integer limit, Collection<Long> ids) throws UmbrellaException {
|
||||
var list = new HashMap<Long, UmbrellaUser>();
|
||||
@@ -423,9 +389,8 @@ CREATE TABLE IF NOT EXISTS {0} (
|
||||
rs.close();
|
||||
} catch (SQLException e) {
|
||||
LOG.log(WARNING,"Failed to load user \"{0}\"!",key,e);
|
||||
|
||||
}
|
||||
if (user == null) throw new UmbrellaException(401,"Failed to load user \"{0}\"!",key);
|
||||
if (user == null) throw new UmbrellaException(HTTP_UNAUTHORIZED,"Failed to load user \"{0}\"!",key);
|
||||
return user;
|
||||
|
||||
}
|
||||
@@ -447,7 +412,7 @@ CREATE TABLE IF NOT EXISTS {0} (
|
||||
}
|
||||
|
||||
public long now() {
|
||||
return LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
|
||||
return LocalDateTime.now().toEpochSecond(UTC);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -516,7 +481,7 @@ CREATE TABLE IF NOT EXISTS {0} (
|
||||
}
|
||||
|
||||
public Instant then(){
|
||||
return LocalDateTime.now().plus(DEFAULT_SESSION_DURATION).toInstant(ZoneOffset.UTC);
|
||||
return LocalDateTime.now().plus(DEFAULT_SESSION_DURATION).toInstant(UTC);
|
||||
}
|
||||
|
||||
private ForeignLogin toForeignLogin(ResultSet rs) throws SQLException {
|
||||
|
||||
Reference in New Issue
Block a user