preparing to pass error messages to client
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -20,6 +20,9 @@ public class Constants {
|
|||||||
public static final String DAYS = "days";
|
public static final String DAYS = "days";
|
||||||
public static final String ENCRYPTION_KEY = "encryption_key";
|
public static final String ENCRYPTION_KEY = "encryption_key";
|
||||||
public static final String ERROR_DESCRIPTION = "error_description";
|
public static final String ERROR_DESCRIPTION = "error_description";
|
||||||
|
public static final String ERROR_LOCKED = "error_locked";
|
||||||
|
public static final String ERROR_LOGIN_FAILED = "error_login_failed";
|
||||||
|
public static final String ERROR_NO_USERNAME = "error_no_username";
|
||||||
public static final String EXPIRATION = "expiration";
|
public static final String EXPIRATION = "expiration";
|
||||||
public static final String EXPIRES_IN = "expires_in";
|
public static final String EXPIRES_IN = "expires_in";
|
||||||
public static final String GRANT_TYPE = "grant_type";
|
public static final String GRANT_TYPE = "grant_type";
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/* © SRSoftware 2024 */
|
||||||
|
package de.srsoftware.oidc.api;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class Error<T> implements Result<T> {
|
||||||
|
private final String cause;
|
||||||
|
private Map<String, Object> metadata;
|
||||||
|
|
||||||
|
public Error(String cause) {
|
||||||
|
this.cause = cause;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String cause() {
|
||||||
|
return cause;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isError() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> Error<T> message(String text) {
|
||||||
|
return new Error<T>(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Error<T> metadata(Object... tokens) {
|
||||||
|
metadata = new HashMap<String, Object>();
|
||||||
|
for (int i = 0; i < tokens.length - 1; i += 2) {
|
||||||
|
metadata.put(tokens[i].toString(), tokens[i + 1]);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/* © SRSoftware 2024 */
|
||||||
|
package de.srsoftware.oidc.api;
|
||||||
|
|
||||||
|
|
||||||
|
public class Payload<T> implements Result<T> {
|
||||||
|
private final T object;
|
||||||
|
|
||||||
|
public Payload(T object) {
|
||||||
|
this.object = object;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> Payload<T> of(T object) {
|
||||||
|
return new Payload<>(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isError() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T get() {
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/* © SRSoftware 2024 */
|
||||||
|
package de.srsoftware.oidc.api;
|
||||||
|
|
||||||
|
public interface Result<T> {
|
||||||
|
public boolean isError();
|
||||||
|
}
|
||||||
@@ -36,9 +36,9 @@ public interface UserService {
|
|||||||
return Optional.of(failedLogin);
|
return Optional.of(failedLogin);
|
||||||
}
|
}
|
||||||
public Optional<User> load(String id);
|
public Optional<User> load(String id);
|
||||||
public Optional<User> login(String username, String password);
|
public Result<User> login(String username, String password);
|
||||||
public default Lock lock(String key) {
|
public default Lock lock(String key) {
|
||||||
return failedLogins.computeIfAbsent(key,k -> new Lock()).count();
|
return failedLogins.computeIfAbsent(key, k -> new Lock()).count();
|
||||||
}
|
}
|
||||||
public boolean passwordMatches(String plaintextPassword, User user);
|
public boolean passwordMatches(String plaintextPassword, User user);
|
||||||
public UserService save(User user);
|
public UserService save(User user);
|
||||||
|
|||||||
@@ -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.login(username, password);
|
Optional<User> user = usersepa.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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
/* © SRSoftware 2024 */
|
/* © SRSoftware 2024 */
|
||||||
package de.srsoftware.oidc.datastore.encrypted;
|
package de.srsoftware.oidc.datastore.encrypted;
|
||||||
|
|
||||||
|
import static de.srsoftware.oidc.api.Constants.*;
|
||||||
import static java.lang.System.Logger.Level.WARNING;
|
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.Error;
|
||||||
|
import de.srsoftware.oidc.api.Payload;
|
||||||
|
import de.srsoftware.oidc.api.Result;
|
||||||
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;
|
||||||
@@ -93,26 +97,28 @@ public class EncryptedUserService extends EncryptedConfig implements UserService
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<User> login(String username, String password) {
|
public Result<User> login(String username, String password) {
|
||||||
if (username == null || username.isBlank()) return empty();
|
if (username == null || username.isBlank()) return Error.message(ERROR_NO_USERNAME);
|
||||||
var optLock = getLock(username);
|
var optLock = getLock(username);
|
||||||
if (optLock.isPresent()) {
|
if (optLock.isPresent()) {
|
||||||
var lock = optLock.get();
|
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());
|
LOG.log(WARNING, "{0} is locked after {1} failed logins. Lock will be released at {2}", username, lock.attempts(), lock.releaseTime());
|
||||||
return empty();
|
Error<User> err = Error.message(ERROR_LOCKED);
|
||||||
|
return err.metadata("attempts", lock.attempts(), "release", lock.releaseTime());
|
||||||
}
|
}
|
||||||
for (var encryptedUser : backend.list()) {
|
for (var encryptedUser : backend.list()) {
|
||||||
var decryptedUser = decrypt(encryptedUser);
|
var decryptedUser = decrypt(encryptedUser);
|
||||||
if (!username.equals(decryptedUser.username())) continue;
|
if (!username.equals(decryptedUser.username())) continue;
|
||||||
if (hasher.matches(password, decryptedUser.hashedPassword())) {
|
if (hasher.matches(password, decryptedUser.hashedPassword())) {
|
||||||
this.unlock(username);
|
this.unlock(username);
|
||||||
return Optional.of(decryptedUser);
|
return Payload.of(decryptedUser);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var lock = lock(username);
|
var lock = lock(username);
|
||||||
LOG.log(WARNING,"Login failed for {0} → locking account until {1}",username,lock.releaseTime());
|
LOG.log(WARNING, "Login failed for {0} → locking account until {1}", username, lock.releaseTime());
|
||||||
return empty();
|
Error<User> err = Error.message(ERROR_LOGIN_FAILED);
|
||||||
|
return err.metadata("release", lock.releaseTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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 static java.lang.System.Logger.Level.WARNING;
|
||||||
|
|
||||||
|
import de.srsoftware.oidc.api.Result;
|
||||||
import de.srsoftware.oidc.api.UserService;
|
import de.srsoftware.oidc.api.UserService;
|
||||||
import de.srsoftware.oidc.api.UserServiceTest;
|
import de.srsoftware.oidc.api.UserServiceTest;
|
||||||
import de.srsoftware.oidc.api.data.AccessToken;
|
import de.srsoftware.oidc.api.data.AccessToken;
|
||||||
@@ -68,7 +69,7 @@ public class EncryptedUserServiceTest extends UserServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<User> login(String username, String password) {
|
public Result<User> login(String username, String password) {
|
||||||
var optLock = getLock(username);
|
var optLock = getLock(username);
|
||||||
if (optLock.isPresent()) {
|
if (optLock.isPresent()) {
|
||||||
var lock = optLock.get();
|
var lock = optLock.get();
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
<div id="content">
|
<div id="content">
|
||||||
<h1>to do…</h1>
|
<h1>to do…</h1>
|
||||||
<ul>
|
<ul>
|
||||||
<li>implement brute-force countermeasures</li>
|
|
||||||
<li>implement token refresh</li>
|
<li>implement token refresh</li>
|
||||||
<li>Configuration im Frontend</li>
|
<li>Configuration im Frontend</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
Reference in New Issue
Block a user