Browse Source

working on backend:

- started FileStore implementation
- implemented placing cookies

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
sqlite
Stephan Richter 4 months ago
parent
commit
c5352ac73b
  1. 1
      de.srsoftware.oidc.api/build.gradle
  2. 42
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Cookie.java
  3. 11
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/PasswordHasher.java
  4. 28
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/PathHandler.java
  5. 9
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionToken.java
  6. 88
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/User.java
  7. 13
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/UserService.java
  8. 1
      de.srsoftware.oidc.app/build.gradle
  9. 16
      de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java
  10. 1
      de.srsoftware.oidc.backend/build.gradle
  11. 47
      de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/Backend.java
  12. 21
      de.srsoftware.oidc.datastore.file/build.gradle
  13. 87
      de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java
  14. 39
      de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/UuidHasher.java
  15. 2
      de.srsoftware.oidc.web/src/main/java/de/srsoftware/oidc/web/StaticPages.java
  16. 6
      de.srsoftware.oidc.web/src/main/resources/en/lightoidc.js
  17. 4
      de.srsoftware.oidc.web/src/main/resources/en/login.html
  18. 1
      settings.gradle

1
de.srsoftware.oidc.api/build.gradle

@ -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 {

42
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Cookie.java

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

11
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/PasswordHasher.java

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

28
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/PathHandler.java

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

9
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionToken.java

@ -0,0 +1,9 @@
/* © SRSoftware 2024 */
package de.srsoftware.oidc.api;
public class SessionToken extends Cookie {
public SessionToken(String value) {
super("sessionToken", value);
}
}

88
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/User.java

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

13
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/UserService.java

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

1
de.srsoftware.oidc.app/build.gradle

@ -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 {

16
de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java

@ -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 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 final String INDEX = STATIC_PATH + "/index.html";
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
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); 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();
} }

1
de.srsoftware.oidc.backend/build.gradle

@ -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 {

47
de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/Backend.java

@ -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

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

87
de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java

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

39
de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/UuidHasher.java

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

2
de.srsoftware.oidc.web/src/main/java/de/srsoftware/oidc/web/StaticPages.java

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

6
de.srsoftware.oidc.web/src/main/resources/en/lightoidc.js

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

4
de.srsoftware.oidc.web/src/main/resources/en/login.html

@ -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>

1
settings.gradle

@ -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'

Loading…
Cancel
Save