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 @@ |
|||||||
|
/* © 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); |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
@ -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(); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue