Browse Source
- started FileStore implementation - implemented placing cookies Signed-off-by: Stephan Richter <s.richter@srsoftware.de>sqlite
Stephan Richter
4 months ago
18 changed files with 399 additions and 26 deletions
@ -0,0 +1,42 @@
@@ -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 @@
@@ -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); |
||||
} |
||||
} |
@ -0,0 +1,9 @@
@@ -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 @@
@@ -1,4 +1,90 @@
|
||||
/* © SRSoftware 2024 */ |
||||
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 @@
@@ -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); |
||||
} |
@ -0,0 +1,21 @@
@@ -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 @@
@@ -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 @@
@@ -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(); |
||||
} |
||||
} |
Loading…
Reference in new issue