working on backend:
- started FileStore implementation - implemented placing cookies Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -11,6 +11,7 @@ repositories {
|
|||||||
dependencies {
|
dependencies {
|
||||||
testImplementation platform('org.junit:junit-bom:5.10.0')
|
testImplementation platform('org.junit:junit-bom:5.10.0')
|
||||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||||
|
implementation 'org.json:json:20240303'
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/* © SRSoftware 2024 */
|
||||||
|
package de.srsoftware.oidc.api;
|
||||||
|
|
||||||
|
import com.sun.net.httpserver.Headers;
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public abstract class Cookie implements Map.Entry<String, String> {
|
||||||
|
private final String key;
|
||||||
|
private String value = null;
|
||||||
|
|
||||||
|
Cookie(String key, String value) {
|
||||||
|
this.key = key;
|
||||||
|
setValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends Cookie> T addTo(Headers headers) {
|
||||||
|
headers.add("Set-Cookie", "%s=%s".formatted(key, value));
|
||||||
|
return (T)this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends Cookie> T addTo(HttpExchange ex) {
|
||||||
|
return this.addTo(ex.getResponseHeaders());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String setValue(String s) {
|
||||||
|
var oldVal = value;
|
||||||
|
value = s;
|
||||||
|
return oldVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/* © SRSoftware 2024 */
|
||||||
|
package de.srsoftware.oidc.api;
|
||||||
|
|
||||||
|
public interface PasswordHasher<T> {
|
||||||
|
public String hash(String password, String salt);
|
||||||
|
public String salt(String hashedPassword);
|
||||||
|
|
||||||
|
public default boolean matches(String plaintextPassword, String hashedPassword) {
|
||||||
|
return hash(plaintextPassword, salt(hashedPassword)).equals(hashedPassword);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
/* © SRSoftware 2024 */
|
/* © SRSoftware 2024 */
|
||||||
package de.srsoftware.oidc.api;
|
package de.srsoftware.oidc.api;
|
||||||
|
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
import com.sun.net.httpserver.HttpExchange;
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
import com.sun.net.httpserver.HttpHandler;
|
import com.sun.net.httpserver.HttpHandler;
|
||||||
import com.sun.net.httpserver.HttpServer;
|
import com.sun.net.httpserver.HttpServer;
|
||||||
@@ -9,10 +11,14 @@ import java.util.Arrays;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
public abstract class PathHandler implements HttpHandler {
|
public abstract class PathHandler implements HttpHandler {
|
||||||
private String path;
|
public static final String CONTENT_TYPE = "Content-Type";
|
||||||
|
public static final String JSON = "application/json";
|
||||||
|
public static final String POST = "POST";
|
||||||
|
|
||||||
|
private String path;
|
||||||
|
|
||||||
public class Bond {
|
public class Bond {
|
||||||
Bond(String p) {
|
Bond(String p) {
|
||||||
@@ -35,15 +41,29 @@ public abstract class PathHandler implements HttpHandler {
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<String> getHeader(HttpExchange ex, String key) {
|
/******* begin of static methods *************/
|
||||||
|
|
||||||
|
public static String body(HttpExchange ex) throws IOException {
|
||||||
|
return new String(ex.getRequestBody().readAllBytes(), UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<String> getAuthToken(HttpExchange ex) {
|
||||||
|
return getHeader(ex, "Authorization");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<String> getHeader(HttpExchange ex, String key) {
|
||||||
return Optional.ofNullable(ex.getRequestHeaders().get(key)).map(List::stream).map(Stream::findFirst).orElse(Optional.empty());
|
return Optional.ofNullable(ex.getRequestHeaders().get(key)).map(List::stream).map(Stream::findFirst).orElse(Optional.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<String> language(HttpExchange ex) {
|
public static JSONObject json(HttpExchange ex) throws IOException {
|
||||||
|
return new JSONObject(body(ex));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<String> language(HttpExchange ex) {
|
||||||
return getHeader(ex, "Accept-Language").map(s -> Arrays.stream(s.split(","))).map(Stream::findFirst).orElse(Optional.empty());
|
return getHeader(ex, "Accept-Language").map(s -> Arrays.stream(s.split(","))).map(Stream::findFirst).orElse(Optional.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void emptyResponse(int statusCode, HttpExchange ex) throws IOException {
|
public static void sendEmptyResponse(int statusCode, HttpExchange ex) throws IOException {
|
||||||
ex.sendResponseHeaders(statusCode, 0);
|
ex.sendResponseHeaders(statusCode, 0);
|
||||||
ex.getResponseBody().close();
|
ex.getResponseBody().close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/* © SRSoftware 2024 */
|
||||||
|
package de.srsoftware.oidc.api;
|
||||||
|
|
||||||
|
|
||||||
|
public class SessionToken extends Cookie {
|
||||||
|
public SessionToken(String value) {
|
||||||
|
super("sessionToken", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,90 @@
|
|||||||
/* © SRSoftware 2024 */
|
/* © SRSoftware 2024 */
|
||||||
package de.srsoftware.oidc.api;
|
package de.srsoftware.oidc.api;
|
||||||
|
|
||||||
public class User {}
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class User {
|
||||||
|
public static final String EMAIL = "email";
|
||||||
|
public static final String PASSWORD = "password";
|
||||||
|
public static final String REALNAME = "realname";
|
||||||
|
public static final String USERNAME = "username";
|
||||||
|
|
||||||
|
private String email, hashedPassword, realName, uuid, username;
|
||||||
|
|
||||||
|
public User(String username, String hashedPassword, String realName, String email, String uuid) {
|
||||||
|
this.username = username;
|
||||||
|
this.realName = realName;
|
||||||
|
this.email = email;
|
||||||
|
this.hashedPassword = hashedPassword;
|
||||||
|
this.uuid = uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String email() {
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User email(String newVal) {
|
||||||
|
email = newVal;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (obj == this) return true;
|
||||||
|
if (obj == null || obj.getClass() != this.getClass()) return false;
|
||||||
|
var that = (User)obj;
|
||||||
|
return Objects.equals(this.uuid, that.uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String hashedPassword() {
|
||||||
|
return hashedPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User hashedPassword(String newValue) {
|
||||||
|
hashedPassword = newValue;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(username, realName, email, hashedPassword, uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Map<String, String> map(boolean includePassword) {
|
||||||
|
return includePassword ? Map.of(USERNAME, username, REALNAME, realName, PASSWORD, hashedPassword, EMAIL, email) : Map.of(USERNAME, username, REALNAME, realName, EMAIL, email);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String realName() {
|
||||||
|
return realName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User realName(String newValue) {
|
||||||
|
realName = newValue;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "User["
|
||||||
|
+ "username=" + username + ", "
|
||||||
|
+ "realName=" + realName + ", "
|
||||||
|
+ "email=" + email + ", "
|
||||||
|
+ "uuid=" + uuid + ']';
|
||||||
|
}
|
||||||
|
|
||||||
|
public String username() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User username(String newVal) {
|
||||||
|
username = newVal;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public String uuid() {
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/* © SRSoftware 2024 */
|
||||||
|
package de.srsoftware.oidc.api;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface UserService {
|
||||||
|
public UserService delete(User user);
|
||||||
|
public UserService init(User defaultUser);
|
||||||
|
public List<User> list();
|
||||||
|
public Optional<User> load(String username, String password);
|
||||||
|
public UserService save(User user);
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ dependencies {
|
|||||||
implementation project(':de.srsoftware.oidc.api')
|
implementation project(':de.srsoftware.oidc.api')
|
||||||
implementation project(':de.srsoftware.oidc.backend')
|
implementation project(':de.srsoftware.oidc.backend')
|
||||||
implementation project(':de.srsoftware.oidc.web')
|
implementation project(':de.srsoftware.oidc.web')
|
||||||
|
implementation project(':de.srsoftware.oidc.datastore.file')
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
|
|||||||
@@ -3,21 +3,35 @@ package de.srsoftware.oidc.app;
|
|||||||
|
|
||||||
|
|
||||||
import com.sun.net.httpserver.HttpServer;
|
import com.sun.net.httpserver.HttpServer;
|
||||||
|
import de.srsoftware.oidc.api.User;
|
||||||
|
import de.srsoftware.oidc.api.UserService;
|
||||||
import de.srsoftware.oidc.backend.Backend;
|
import de.srsoftware.oidc.backend.Backend;
|
||||||
|
import de.srsoftware.oidc.datastore.file.FileStore;
|
||||||
|
import de.srsoftware.oidc.datastore.file.UuidHasher;
|
||||||
import de.srsoftware.oidc.web.Forward;
|
import de.srsoftware.oidc.web.Forward;
|
||||||
import de.srsoftware.oidc.web.StaticPages;
|
import de.srsoftware.oidc.web.StaticPages;
|
||||||
|
import java.io.File;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
public class Application {
|
public class Application {
|
||||||
public static final String STATIC_PATH = "/web";
|
public static final String STATIC_PATH = "/web";
|
||||||
public static final String INDEX = STATIC_PATH + "/index.html";
|
public static final String FIRST_USER = "admin";
|
||||||
|
public static final String FIRST_USER_PASS = "admin";
|
||||||
|
public static final String FIRST_UUID = UUID.randomUUID().toString();
|
||||||
|
public static final String INDEX = STATIC_PATH + "/index.html";
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
|
var storageFile = new File("/tmp/lightoidc.json");
|
||||||
|
var passwordHasher = new UuidHasher();
|
||||||
|
var firstHash = passwordHasher.hash(FIRST_USER_PASS, FIRST_UUID);
|
||||||
|
var firstUser = new User(FIRST_USER, firstHash, FIRST_USER, "%s@internal".formatted(FIRST_USER), FIRST_UUID);
|
||||||
|
UserService userService = new FileStore(storageFile, passwordHasher).init(firstUser);
|
||||||
|
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
|
||||||
new StaticPages().bindPath(STATIC_PATH).on(server);
|
new StaticPages().bindPath(STATIC_PATH).on(server);
|
||||||
new Forward(INDEX).bindPath("/").on(server);
|
new Forward(INDEX).bindPath("/").on(server);
|
||||||
new Backend().bindPath("/api").on(server);
|
new Backend(userService).bindPath("/api").on(server);
|
||||||
server.setExecutor(Executors.newCachedThreadPool());
|
server.setExecutor(Executors.newCachedThreadPool());
|
||||||
server.start();
|
server.start();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ dependencies {
|
|||||||
testImplementation platform('org.junit:junit-bom:5.10.0')
|
testImplementation platform('org.junit:junit-bom:5.10.0')
|
||||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||||
implementation project(':de.srsoftware.oidc.api')
|
implementation project(':de.srsoftware.oidc.api')
|
||||||
|
implementation 'org.json:json:20240303'
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
|
|||||||
@@ -1,28 +1,56 @@
|
|||||||
/* © SRSoftware 2024 */
|
/* © SRSoftware 2024 */
|
||||||
package de.srsoftware.oidc.backend;
|
package de.srsoftware.oidc.backend;
|
||||||
|
|
||||||
|
import static de.srsoftware.oidc.api.User.PASSWORD;
|
||||||
|
import static de.srsoftware.oidc.api.User.USERNAME;
|
||||||
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
|
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
|
||||||
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
|
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
import static java.util.jar.Attributes.Name.CONTENT_TYPE;
|
||||||
|
|
||||||
import com.sun.net.httpserver.HttpExchange;
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
import de.srsoftware.oidc.api.PathHandler;
|
import de.srsoftware.oidc.api.PathHandler;
|
||||||
|
import de.srsoftware.oidc.api.SessionToken;
|
||||||
|
import de.srsoftware.oidc.api.User;
|
||||||
|
import de.srsoftware.oidc.api.UserService;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
public class Backend extends PathHandler {
|
public class Backend extends PathHandler {
|
||||||
|
private final UserService users;
|
||||||
|
|
||||||
|
public Backend(UserService userService) {
|
||||||
|
users = userService;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doLogin(HttpExchange ex) throws IOException {
|
||||||
|
var body = json(ex);
|
||||||
|
|
||||||
|
var username = body.has(USERNAME) ? body.getString(USERNAME) : null;
|
||||||
|
var password = body.has(PASSWORD) ? body.getString(PASSWORD) : null;
|
||||||
|
|
||||||
|
Optional<User> user = users.load(username, password);
|
||||||
|
if (user.isPresent()) {
|
||||||
|
sendUserAndCookie(ex, user.get());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendEmptyResponse(HTTP_UNAUTHORIZED, ex);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handle(HttpExchange ex) throws IOException {
|
public void handle(HttpExchange ex) throws IOException {
|
||||||
String path = relativePath(ex);
|
String path = relativePath(ex);
|
||||||
String method = ex.getRequestMethod();
|
String method = ex.getRequestMethod();
|
||||||
System.out.printf("%s %s…", method, path);
|
System.out.printf("%s %s…", method, path);
|
||||||
|
|
||||||
if ("login".equals(path)) {
|
if ("login".equals(path) && POST.equals(method)) {
|
||||||
doLogin(ex); // TODO: prevent brute force
|
doLogin(ex); // TODO: prevent brute force
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var token = getAuthToken(ex);
|
var token = getAuthToken(ex);
|
||||||
if (token.isEmpty()) {
|
if (token.isEmpty()) {
|
||||||
emptyResponse(HTTP_UNAUTHORIZED, ex);
|
sendEmptyResponse(HTTP_UNAUTHORIZED, ex);
|
||||||
System.err.println("unauthorized");
|
System.err.println("unauthorized");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -31,14 +59,13 @@ public class Backend extends PathHandler {
|
|||||||
ex.getResponseBody().close();
|
ex.getResponseBody().close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doLogin(HttpExchange ex) throws IOException {
|
private void sendUserAndCookie(HttpExchange ex, User user) throws IOException {
|
||||||
Optional<String> user = getHeader(ex, "login-username");
|
var bytes = new JSONObject(user.map(false)).toString().getBytes(UTF_8);
|
||||||
Optional<String> pass = getHeader(ex, "login-password");
|
var headers = ex.getResponseHeaders();
|
||||||
System.out.printf("%s : %s", user, pass);
|
|
||||||
emptyResponse(HTTP_UNAUTHORIZED, ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<String> getAuthToken(HttpExchange ex) {
|
headers.add(CONTENT_TYPE, JSON);
|
||||||
return getHeader(ex, "Authorization");
|
new SessionToken("Test").addTo(headers);
|
||||||
|
ex.sendResponseHeaders(200, bytes.length);
|
||||||
|
ex.getResponseBody().write(bytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
de.srsoftware.oidc.datastore.file/build.gradle
Normal file
21
de.srsoftware.oidc.datastore.file/build.gradle
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
}
|
||||||
|
|
||||||
|
group = 'de.srsoftware'
|
||||||
|
version = '1.0-SNAPSHOT'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation platform('org.junit:junit-bom:5.10.0')
|
||||||
|
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||||
|
implementation project(':de.srsoftware.oidc.api')
|
||||||
|
implementation 'org.json:json:20240303'
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/* © SRSoftware 2024 */
|
||||||
|
package de.srsoftware.oidc.datastore.file; /* © SRSoftware 2024 */
|
||||||
|
import static de.srsoftware.oidc.api.User.*;
|
||||||
|
|
||||||
|
import de.srsoftware.oidc.api.PasswordHasher;
|
||||||
|
import de.srsoftware.oidc.api.User;
|
||||||
|
import de.srsoftware.oidc.api.UserService;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
public class FileStore implements UserService {
|
||||||
|
private static final String USERS = "users";
|
||||||
|
|
||||||
|
private final Path storageFile;
|
||||||
|
private final JSONObject json;
|
||||||
|
private final PasswordHasher<String> passwordHasher;
|
||||||
|
|
||||||
|
public FileStore(File storage, PasswordHasher<String> passwordHasher) throws IOException {
|
||||||
|
this.storageFile = storage.toPath();
|
||||||
|
this.passwordHasher = passwordHasher;
|
||||||
|
|
||||||
|
if (!storage.exists()) {
|
||||||
|
var parent = storage.getParentFile();
|
||||||
|
if (!parent.exists() && !parent.mkdirs()) throw new FileNotFoundException("Failed to create directory %s".formatted(parent));
|
||||||
|
Files.writeString(storageFile, "{}");
|
||||||
|
}
|
||||||
|
json = new JSONObject(Files.readString(storageFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<User> load(String username, String password) {
|
||||||
|
try {
|
||||||
|
var users = json.getJSONObject(USERS);
|
||||||
|
var uuids = users.keySet();
|
||||||
|
for (String uuid : uuids) {
|
||||||
|
var user = users.getJSONObject(uuid);
|
||||||
|
if (!user.getString(USERNAME).equals(username)) continue;
|
||||||
|
var hashedPass = user.getString(PASSWORD);
|
||||||
|
if (passwordHasher.matches(password, hashedPass)) {
|
||||||
|
return Optional.of(new User(username, hashedPass, user.getString(REALNAME), user.getString(EMAIL), uuid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserService delete(User user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserService init(User defaultUser) {
|
||||||
|
if (!json.has(USERS)) save(defaultUser);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserService save(User user) {
|
||||||
|
JSONObject users;
|
||||||
|
if (!json.has(USERS)) {
|
||||||
|
json.put(USERS, users = new JSONObject());
|
||||||
|
} else
|
||||||
|
users = json.getJSONObject(USERS);
|
||||||
|
users.put(user.uuid(), user.map(true));
|
||||||
|
try {
|
||||||
|
Files.writeString(storageFile, json.toString(2));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<User> list() {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/* © SRSoftware 2024 */
|
||||||
|
package de.srsoftware.oidc.datastore.file;
|
||||||
|
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
|
import de.srsoftware.oidc.api.PasswordHasher;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
|
||||||
|
public class UuidHasher implements PasswordHasher<String> {
|
||||||
|
private static final String SHA256 = "SHA-256";
|
||||||
|
|
||||||
|
private final MessageDigest digest;
|
||||||
|
|
||||||
|
public UuidHasher() throws NoSuchAlgorithmException {
|
||||||
|
digest = MessageDigest.getInstance(SHA256);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String hash(String password, String uuid) {
|
||||||
|
var salt = uuid;
|
||||||
|
var saltedPass = "%s %s".formatted(salt, password);
|
||||||
|
var bytes = digest.digest(saltedPass.getBytes(UTF_8));
|
||||||
|
|
||||||
|
return "%s@%s".formatted(hex(bytes), salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String salt(String hashedPassword) {
|
||||||
|
return hashedPassword.split("@")[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String hex(byte[] bytes) {
|
||||||
|
StringBuilder sb = new StringBuilder(bytes.length * 2);
|
||||||
|
for (byte b : bytes) sb.append(String.format("%02x", b));
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ public class StaticPages extends PathHandler {
|
|||||||
System.out.printf("Loading %s for lagnuage %s…", path, lang);
|
System.out.printf("Loading %s for lagnuage %s…", path, lang);
|
||||||
var response = loadTemplate(lang, path).orElseThrow(() -> new FileNotFoundException());
|
var response = loadTemplate(lang, path).orElseThrow(() -> new FileNotFoundException());
|
||||||
|
|
||||||
ex.getResponseHeaders().add("Content-Type", response.contentType);
|
ex.getResponseHeaders().add(CONTENT_TYPE, response.contentType);
|
||||||
ex.sendResponseHeaders(200, response.content.length);
|
ex.sendResponseHeaders(200, response.content.length);
|
||||||
OutputStream os = ex.getResponseBody();
|
OutputStream os = ex.getResponseBody();
|
||||||
os.write(response.content);
|
os.write(response.content);
|
||||||
|
|||||||
@@ -33,11 +33,11 @@ function tryLogin(){
|
|||||||
document.getElementById("error").innerHTML = "";
|
document.getElementById("error").innerHTML = "";
|
||||||
var data = Object.fromEntries(new FormData(document.getElementById('login')));
|
var data = Object.fromEntries(new FormData(document.getElementById('login')));
|
||||||
fetch(api+"/login",{
|
fetch(api+"/login",{
|
||||||
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'login-username': data.user,
|
|
||||||
'login-password': data.pass, // TODO: send via body?
|
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
}).then(handleLogin);
|
}).then(handleLogin);
|
||||||
}
|
}
|
||||||
@@ -11,11 +11,11 @@
|
|||||||
<legend>User credentials</legend>
|
<legend>User credentials</legend>
|
||||||
<label>
|
<label>
|
||||||
Username
|
Username
|
||||||
<input type="text" name="user" />
|
<input type="text" name="username" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Password
|
Password
|
||||||
<input type="password" name="pass" />
|
<input type="password" name="password" />
|
||||||
</label>
|
</label>
|
||||||
<button type="button" onClick="tryLogin()">Login</button>
|
<button type="button" onClick="tryLogin()">Login</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ include 'de.srsoftware.oidc.api'
|
|||||||
include 'de.srsoftware.oidc.app'
|
include 'de.srsoftware.oidc.app'
|
||||||
include 'de.srsoftware.oidc.web'
|
include 'de.srsoftware.oidc.web'
|
||||||
include 'de.srsoftware.oidc.backend'
|
include 'de.srsoftware.oidc.backend'
|
||||||
|
include 'de.srsoftware.oidc.datastore.file'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user