implemented brute force counter measure
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user