Browse Source

implemented brute force counter measure

feature/brute_force_protection
Stephan Richter 2 months ago
parent
commit
e1f32c274b
  1. 9
      core/src/main/java/de/srsoftware/umbrella/core/ConnectionProvider.java
  2. 12
      frontend/src/Components/Login.svelte
  3. 4
      frontend/src/user.svelte.js
  4. 1
      translations/src/main/resources/de.json
  5. 5
      user/src/main/java/de/srsoftware/umbrella/user/Constants.java
  6. 9
      user/src/main/java/de/srsoftware/umbrella/user/UserModule.java
  7. 60
      user/src/main/java/de/srsoftware/umbrella/user/model/Lock.java
  8. 73
      user/src/main/java/de/srsoftware/umbrella/user/sqlite/SqliteDB.java

9
core/src/main/java/de/srsoftware/umbrella/core/ConnectionProvider.java

@ -1,6 +1,8 @@
/* © SRSoftware 2025 */ /* © SRSoftware 2025 */
package de.srsoftware.umbrella.core; package de.srsoftware.umbrella.core;
import static java.lang.System.Logger.Level.INFO;
import java.io.File; import java.io.File;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
@ -8,7 +10,7 @@ import java.util.HashMap;
import org.sqlite.SQLiteDataSource; import org.sqlite.SQLiteDataSource;
public class ConnectionProvider { public class ConnectionProvider {
private static final System.Logger LOG = System.getLogger(ConnectionProvider.class.getSimpleName());
private static final HashMap<File, Connection> connections = new HashMap<>(); private static final HashMap<File, Connection> connections = new HashMap<>();
private ConnectionProvider(){} private ConnectionProvider(){}
@ -17,7 +19,10 @@ public class ConnectionProvider {
if (o instanceof String filename) o = new File(filename); if (o instanceof String filename) o = new File(filename);
if (o instanceof File dbFile) try { if (o instanceof File dbFile) try {
var conn = connections.get(dbFile); 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; return conn;
} catch (SQLException sqle) { } catch (SQLException sqle) {
throw new RuntimeException(sqle); throw new RuntimeException(sqle);

12
frontend/src/Components/Login.svelte

@ -9,11 +9,16 @@
let credentials = $state({ username : null, password : null }); let credentials = $state({ username : null, password : null });
const router = useTinyRouter(); const router = useTinyRouter();
let services = $state([]); let services = $state([]);
let error = $state(null);
function doLogin(ev){ async function doLogin(ev){
ev.preventDefault(); 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){ function init(element){
@ -76,6 +81,9 @@
<form onsubmit={doLogin}> <form onsubmit={doLogin}>
<fieldset> <fieldset>
{#if error}
<span class="error">{error}</span>
{/if}
<legend>{t('login')}</legend> <legend>{t('login')}</legend>
<label> <label>
<input type="text" bind:value={credentials.username} required use:init /> <input type="text" bind:value={credentials.username} required use:init />

4
frontend/src/user.svelte.js

@ -43,8 +43,10 @@ export async function tryLogin(credentials){
if (response.ok){ if (response.ok){
const json = await response.json(); const json = await response.json();
for (let key of Object.keys(json)) user[key] = json[key]; for (let key of Object.keys(json)) user[key] = json[key];
return null;
} else { } else {
alert("Login failed!"); alert("Login failed!");
let json = await response.json();
return json;
} }
} }

1
translations/src/main/resources/de.json

@ -72,6 +72,7 @@
"extended_settings": "erweiterte Einstellungen", "extended_settings": "erweiterte Einstellungen",
"failed": "fehlgeschlagen", "failed": "fehlgeschlagen",
"failed_login_attempts" : "Account nach {attempts} fehlgeschlagenen Logins gesperrt bis {release_time}",
"files": "Dateien", "files": "Dateien",
"filter": "Filter", "filter": "Filter",
"footer": "Fuß-Text", "footer": "Fuß-Text",

5
user/src/main/java/de/srsoftware/umbrella/user/Constants.java

@ -16,22 +16,21 @@ public class Constants {
public static final String DEFAULT_FIELD = "sub"; public static final String DEFAULT_FIELD = "sub";
public static final Duration DEFAULT_SESSION_DURATION = Duration.ofMinutes(20); 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 FOREIGN_ID = "foreign_id";
public static final String GRANT_TYPE = "grant_type"; 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 ID_TOKEN = "id_token";
public static final String JWKS_ENDPOINT = "jwks_uri"; public static final String JWKS_ENDPOINT = "jwks_uri";
public static final String LAST_LOGOFF = "last_logoff"; 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 MESSAGE_DELIVERY = "message_delivery";
public static final String OIDC = "oidc"; public static final String OIDC = "oidc";
public static final String OIDC_CALLBACK = "redirect_uri"; 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 OIDC_SCOPE = "openid";
public static final String REDIRECT_URI = "redirect_uri"; 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 RESPONSE_TYPE = "response_type";
public static final String SCOPE = "scope"; public static final String SCOPE = "scope";
public static final String SERVICE_ID = "service_id"; public static final String SERVICE_ID = "service_id";

9
user/src/main/java/de/srsoftware/umbrella/user/UserModule.java

@ -513,14 +513,23 @@ public class UserModule extends BaseHandler implements UserService {
private boolean postLogin(HttpExchange ex) throws IOException { private boolean postLogin(HttpExchange ex) throws IOException {
var json = json(ex); var json = json(ex);
if (!(json.has(USERNAME) && json.get(USERNAME) instanceof String username)) return sendContent(ex, HTTP_UNPROCESSABLE,"Username missing"); 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 (!(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"); if (password.isBlank()) return sendContent(ex,HTTP_UNAUTHORIZED,"Password must not be blank");
var hashedPass = Password.of(BAD_HASHER.hash(password,null)); var hashedPass = Password.of(BAD_HASHER.hash(password,null));
try { try {
var user = users.load(username, hashedPass); var user = users.load(username, hashedPass);
Lock.unlock(username);
users.getSession(user).cookie().addTo(ex); users.getSession(user).cookie().addTo(ex);
return sendContent(ex,user); return sendContent(ex,user);
} catch (UmbrellaException e){ } catch (UmbrellaException e){
if (e.statusCode() == HTTP_UNAUTHORIZED) return sendContent(ex,HTTP_UNAUTHORIZED,Lock.lock(username));
return send(ex,e); return send(ex,e);
} }
} }

60
user/src/main/java/de/srsoftware/umbrella/user/model/Lock.java

@ -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);
}
}

73
user/src/main/java/de/srsoftware/umbrella/user/sqlite/SqliteDB.java

@ -8,15 +8,15 @@ import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.user.Constants.*; import static de.srsoftware.umbrella.user.Constants.*;
import static de.srsoftware.umbrella.user.model.DbUser.ADMIN_PERMISSIONS; import static de.srsoftware.umbrella.user.model.DbUser.ADMIN_PERMISSIONS;
import static java.lang.System.Logger.Level.*; import static java.lang.System.Logger.Level.*;
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
import static java.text.MessageFormat.format; import static java.text.MessageFormat.format;
import static java.time.ZoneOffset.UTC;
import de.srsoftware.tools.PasswordHasher; import de.srsoftware.tools.PasswordHasher;
import de.srsoftware.tools.jdbc.Query; import de.srsoftware.tools.jdbc.Query;
import de.srsoftware.umbrella.core.BaseDb;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.EmailAddress; import de.srsoftware.umbrella.core.model.*;
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.user.BadHasher; import de.srsoftware.umbrella.user.BadHasher;
import de.srsoftware.umbrella.user.api.LoginServiceDb; import de.srsoftware.umbrella.user.api.LoginServiceDb;
import de.srsoftware.umbrella.user.api.UserDb; import de.srsoftware.umbrella.user.api.UserDb;
@ -29,20 +29,15 @@ import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.*; 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 System.Logger LOG = System.getLogger(SqliteDB.class.getSimpleName());
private static final int INITIAL_DB_VERSION = 4; private static final int INITIAL_DB_VERSION = 4;
private HashMap<String,Session> sessions = new HashMap<>();
private final Connection db;
public SqliteDB(Connection conn){ public SqliteDB(Connection conn){
db = conn; super(conn);
init();
} }
@ -94,43 +89,21 @@ CREATE TABLE IF NOT EXISTS {0} (
} }
} }
protected int createTables() {
int currentVersion = createSettingsTable();
private int createSettingsTable() { switch (currentVersion){
var createTable = """ case 0:
CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255) NOT NULL); case 1:
"""; case 2:
try { case 3:
var stmt = db.prepareStatement(format(createTable,TABLE_SETTINGS, KEY, VALUE)); createUserTables();
stmt.execute(); createLoginServiceTables();
stmt.close();
} catch (SQLException e) {
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_SETTINGS,e);
throw new RuntimeException(e);
} }
Integer version = null; return setCurrentVersion(4);
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 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() { private void createUserTables() {
PasswordHasher<String> hasher; PasswordHasher<String> hasher;
@ -287,13 +260,6 @@ CREATE TABLE IF NOT EXISTS {0} (
return userId; return userId;
} }
private void init(){
var version = createTables();
}
@Override @Override
public Map<Long, UmbrellaUser> list(Integer start, Integer limit, Collection<Long> ids) throws UmbrellaException { public Map<Long, UmbrellaUser> list(Integer start, Integer limit, Collection<Long> ids) throws UmbrellaException {
var list = new HashMap<Long, UmbrellaUser>(); var list = new HashMap<Long, UmbrellaUser>();
@ -423,9 +389,8 @@ CREATE TABLE IF NOT EXISTS {0} (
rs.close(); rs.close();
} catch (SQLException e) { } catch (SQLException e) {
LOG.log(WARNING,"Failed to load user \"{0}\"!",key,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; return user;
} }
@ -447,7 +412,7 @@ CREATE TABLE IF NOT EXISTS {0} (
} }
public long now() { public long now() {
return LocalDateTime.now().toEpochSecond(ZoneOffset.UTC); return LocalDateTime.now().toEpochSecond(UTC);
} }
@Override @Override
@ -516,7 +481,7 @@ CREATE TABLE IF NOT EXISTS {0} (
} }
public Instant then(){ 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 { private ForeignLogin toForeignLogin(ResultSet rs) throws SQLException {

Loading…
Cancel
Save