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 ENCRYPTION_KEY = "encryption_key";
|
||||
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 EXPIRES_IN = "expires_in";
|
||||
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();
|
||||
}
|
||||
@@ -26,19 +26,19 @@ public interface UserService {
|
||||
* @param accessToken
|
||||
* @return
|
||||
*/
|
||||
public Optional<User> forToken(String accessToken);
|
||||
public UserService init(User defaultUser);
|
||||
public List<User> list();
|
||||
public Set<User> find(String idOrEmail);
|
||||
public Optional<User> forToken(String accessToken);
|
||||
public UserService init(User defaultUser);
|
||||
public List<User> list();
|
||||
public Set<User> find(String idOrEmail);
|
||||
public default Optional<Lock> getLock(String key) {
|
||||
var failedLogin = failedLogins.get(key);
|
||||
if (failedLogin == null || failedLogin.releaseTime().isBefore(Instant.now())) return empty();
|
||||
return Optional.of(failedLogin);
|
||||
}
|
||||
public Optional<User> load(String id);
|
||||
public Optional<User> login(String username, String password);
|
||||
public default Lock lock(String key) {
|
||||
return failedLogins.computeIfAbsent(key,k -> new Lock()).count();
|
||||
public Optional<User> load(String id);
|
||||
public Result<User> login(String username, String password);
|
||||
public default Lock lock(String key) {
|
||||
return failedLogins.computeIfAbsent(key, k -> new Lock()).count();
|
||||
}
|
||||
public boolean passwordMatches(String plaintextPassword, User user);
|
||||
public UserService save(User user);
|
||||
|
||||
@@ -4,8 +4,8 @@ package de.srsoftware.oidc.api.data;
|
||||
import java.time.Instant;
|
||||
|
||||
public class Lock {
|
||||
private int attempts;
|
||||
private Instant releaseTime;
|
||||
private int attempts;
|
||||
private Instant releaseTime;
|
||||
|
||||
public Lock() {
|
||||
this.attempts = 0;
|
||||
|
||||
@@ -195,7 +195,7 @@ public class UserController extends Controller {
|
||||
var password = body.has(PASSWORD) ? body.getString(PASSWORD) : null;
|
||||
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());
|
||||
return sendEmptyResponse(HTTP_UNAUTHORIZED, ex);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
/* © SRSoftware 2024 */
|
||||
package de.srsoftware.oidc.datastore.encrypted;
|
||||
|
||||
import static de.srsoftware.oidc.api.Constants.*;
|
||||
import static java.lang.System.Logger.Level.WARNING;
|
||||
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.data.AccessToken;
|
||||
import de.srsoftware.oidc.api.data.User;
|
||||
@@ -93,26 +97,28 @@ public class EncryptedUserService extends EncryptedConfig implements UserService
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<User> login(String username, String password) {
|
||||
if (username == null || username.isBlank()) return empty();
|
||||
public Result<User> login(String username, String password) {
|
||||
if (username == null || username.isBlank()) return Error.message(ERROR_NO_USERNAME);
|
||||
var optLock = getLock(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 empty();
|
||||
Error<User> err = Error.message(ERROR_LOCKED);
|
||||
return err.metadata("attempts", lock.attempts(), "release", lock.releaseTime());
|
||||
}
|
||||
for (var encryptedUser : backend.list()) {
|
||||
var decryptedUser = decrypt(encryptedUser);
|
||||
if (!username.equals(decryptedUser.username())) continue;
|
||||
if (hasher.matches(password, decryptedUser.hashedPassword())) {
|
||||
this.unlock(username);
|
||||
return Optional.of(decryptedUser);
|
||||
return Payload.of(decryptedUser);
|
||||
}
|
||||
}
|
||||
|
||||
var lock = lock(username);
|
||||
LOG.log(WARNING,"Login failed for {0} → locking account until {1}",username,lock.releaseTime());
|
||||
return empty();
|
||||
LOG.log(WARNING, "Login failed for {0} → locking account until {1}", username, lock.releaseTime());
|
||||
Error<User> err = Error.message(ERROR_LOGIN_FAILED);
|
||||
return err.metadata("release", lock.releaseTime());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -3,6 +3,7 @@ import static de.srsoftware.utils.Optionals.nullable;
|
||||
import static de.srsoftware.utils.Strings.uuid;
|
||||
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.UserServiceTest;
|
||||
import de.srsoftware.oidc.api.data.AccessToken;
|
||||
@@ -68,7 +69,7 @@ public class EncryptedUserServiceTest extends UserServiceTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<User> login(String username, String password) {
|
||||
public Result<User> login(String username, String password) {
|
||||
var optLock = getLock(username);
|
||||
if (optLock.isPresent()) {
|
||||
var lock = optLock.get();
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
<div id="content">
|
||||
<h1>to do…</h1>
|
||||
<ul>
|
||||
<li>implement brute-force countermeasures</li>
|
||||
<li>implement token refresh</li>
|
||||
<li>Configuration im Frontend</li>
|
||||
</ul>
|
||||
|
||||
Reference in New Issue
Block a user