diff --git a/de.srsoftware.oidc.app/build.gradle b/de.srsoftware.oidc.app/build.gradle index 8007b12..d7c9ed9 100644 --- a/de.srsoftware.oidc.app/build.gradle +++ b/de.srsoftware.oidc.app/build.gradle @@ -21,6 +21,8 @@ dependencies { implementation project(':de.srsoftware.utils') implementation project(':de.srsoftware.oidc.datastore.file') implementation project(':de.srsoftware.oidc.datastore.sqlite') + implementation 'org.json:json:20240303' + } test { diff --git a/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java b/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java index 0983157..c13213d 100644 --- a/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java +++ b/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java @@ -6,6 +6,7 @@ import static de.srsoftware.oidc.api.Constants.*; import static de.srsoftware.oidc.api.data.Permission.*; import static de.srsoftware.utils.Optionals.emptyIfBlank; import static de.srsoftware.utils.Paths.configDir; +import static de.srsoftware.utils.Paths.extension; import static de.srsoftware.utils.Strings.uuid; import static java.lang.System.Logger.Level.DEBUG; import static java.lang.System.Logger.Level.ERROR; @@ -14,21 +15,21 @@ import static java.util.Optional.empty; import com.sun.net.httpserver.HttpServer; import de.srsoftware.logging.ColorLogger; -import de.srsoftware.oidc.api.KeyManager; -import de.srsoftware.oidc.api.KeyStorage; +import de.srsoftware.oidc.api.*; import de.srsoftware.oidc.api.data.User; import de.srsoftware.oidc.backend.*; import de.srsoftware.oidc.datastore.file.FileStore; +import de.srsoftware.oidc.datastore.file.FileStoreProvider; import de.srsoftware.oidc.datastore.file.PlaintextKeyStore; import de.srsoftware.oidc.datastore.file.UuidHasher; -import de.srsoftware.oidc.datastore.sqlite.SqliteKeyStore; +import de.srsoftware.oidc.datastore.sqlite.*; import de.srsoftware.oidc.web.Forward; import de.srsoftware.oidc.web.StaticPages; +import java.io.File; import java.net.InetSocketAddress; import java.nio.file.Path; import java.util.*; import java.util.concurrent.Executors; -import org.sqlite.SQLiteDataSource; public class Application { public static final String API_CLIENT = "/api/client"; @@ -42,41 +43,76 @@ public class Application { public static final String ROOT = "/"; public static final String STATIC_PATH = "/web"; - private static final String BASE_PATH = "basePath"; - private static final String FAVICON = "/favicon.ico"; - private static final String INDEX = STATIC_PATH + "/index.html"; - private static final String WELL_KNOWN = "/.well-known"; - private static System.Logger LOG = new ColorLogger("Application").setLogLevel(DEBUG); - - public static void main(String[] args) throws Exception { - var argMap = map(args); - Optional basePath = argMap.get(BASE_PATH) instanceof Path p ? Optional.of(p) : empty(); - var configFile = (argMap.get(CONFIG_PATH) instanceof Path p ? p : configDir(APP_NAME).resolve("config.json")).toFile(); - var storageFile = configDir(APP_NAME).resolve("data.json").toFile(); - var keyDir = storageFile.getParentFile().toPath().resolve("keys"); - 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).add(MANAGE_CLIENTS, MANAGE_PERMISSIONS, MANAGE_SMTP, MANAGE_USERS); - KeyStorage keyStore = new PlaintextKeyStore(keyDir); - { // SQLite - SQLiteDataSource dataSource = new SQLiteDataSource(); - var dbFile = storageFile.getParentFile().toPath().resolve("db.sqlite3").toFile(); - dataSource.setUrl("jdbc:sqlite:%s".formatted(dbFile)); - var conn = dataSource.getConnection(); - keyStore = new SqliteKeyStore(conn); + private static final String BASE_PATH = "basePath"; + private static final String FAVICON = "/favicon.ico"; + private static final String INDEX = STATIC_PATH + "/index.html"; + private static final String WELL_KNOWN = "/.well-known"; + private static System.Logger LOG = new ColorLogger("Application").setLogLevel(DEBUG); + private static ConnectionProvider connectionProvider = new ConnectionProvider(); + public static void main(String[] args) throws Exception { + + var argMap = map(args); + Optional basePath = argMap.get(BASE_PATH) instanceof Path p ? Optional.of(p) : empty(); + var configFile = (argMap.get(CONFIG_PATH) instanceof Path p ? p : configDir(APP_NAME).resolve("config.json")).toFile(); + var config = new Configuration(configFile); + var defaultConfigDir = configDir(APP_NAME); + 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).add(MANAGE_CLIENTS, MANAGE_PERMISSIONS, MANAGE_SMTP, MANAGE_USERS); + var defaultFile = defaultConfigDir.resolve("data.json"); + var keyStorageLocation = new File(config.getOrDefault("key_storage", defaultConfigDir.resolve("keys"))); + KeyStorage keyStore; + if ((keyStorageLocation.exists() && keyStorageLocation.isDirectory()) || !keyStorageLocation.getName().contains(".")) { + keyStore = new PlaintextKeyStore(keyStorageLocation.toPath()); + } else { // SQLite + var conn = connectionProvider.get(keyStorageLocation); + keyStore = new SqliteKeyStore(conn); } + KeyManager keyManager = new RotatingKeyManager(keyStore); - FileStore fileStore = new FileStore(storageFile, passwordHasher).init(firstUser); + FileStoreProvider fileStoreProvider = new FileStoreProvider(passwordHasher); + + var userStorageLocation = new File(config.getOrDefault("user_storage",defaultFile)); + var userService = switch (extension(userStorageLocation).toLowerCase()){ + case "db", "sqlite", "sqlite3" -> new SqliteUserService(connectionProvider.get(userStorageLocation)); + default -> fileStoreProvider.get(userStorageLocation); + }; + userService.init(firstUser); + + var mailConfigLocation = new File(config.getOrDefault("mail_config_storage",defaultFile)); + var mailConfig = switch (extension(mailConfigLocation)){ + case "db", "sqlite", "sqlite3" -> new SqliteMailConfig(connectionProvider.get(userStorageLocation)); + default -> fileStoreProvider.get(mailConfigLocation); + }; + + var sessionStore = new File(config.getOrDefault("session_storage",defaultFile)); + var sessionService = switch (extension(sessionStore)){ + case "db", "sqlite", "sqlite3" -> new SqliteSessionService(connectionProvider.get(sessionStore)); + default -> fileStoreProvider.get(sessionStore); + }; + + var authServiceLocation = new File(config.getOrDefault("auth_store",defaultFile)); + AuthorizationService authService = switch (extension(authServiceLocation)){ + case "db", "sqlite", "sqlite3" -> new SqliteAuthService(connectionProvider.get(sessionStore)); + default -> fileStoreProvider.get(sessionStore); + }; + + var clientStore = new File(config.getOrDefault("client_store",defaultFile)); + ClientService clientService = switch (extension(clientStore)){ + case "db", "sqlite", "sqlite3" -> new SqliteClientService(connectionProvider.get(sessionStore)); + default -> fileStoreProvider.get(sessionStore); + }; + HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); var staticPages = (StaticPages) new StaticPages(basePath).bindPath(STATIC_PATH, FAVICON).on(server); new Forward(INDEX).bindPath(ROOT).on(server); new WellKnownController().bindPath(WELL_KNOWN).on(server); - new UserController(fileStore, fileStore, fileStore, staticPages).bindPath(API_USER).on(server); + new UserController(mailConfig, sessionService, userService, staticPages).bindPath(API_USER).on(server); var tokenControllerConfig = new TokenController.Configuration("https://lightoidc.srsoftware.de", 10); // TODO configure or derive from hostname - new TokenController(fileStore, fileStore, keyManager, fileStore, tokenControllerConfig).bindPath(API_TOKEN).on(server); - new ClientController(fileStore, fileStore, fileStore).bindPath(API_CLIENT).on(server); + new TokenController(authService, clientService, keyManager, userService, tokenControllerConfig).bindPath(API_TOKEN).on(server); + new ClientController(authService, clientService, sessionService).bindPath(API_CLIENT).on(server); new KeyStoreController(keyStore).bindPath(JWKS).on(server); - new EmailController(fileStore, fileStore).bindPath(API_EMAIL).on(server); + new EmailController(mailConfig, sessionService).bindPath(API_EMAIL).on(server); server.setExecutor(Executors.newCachedThreadPool()); server.start(); } diff --git a/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Configuration.java b/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Configuration.java new file mode 100644 index 0000000..c8dbcc0 --- /dev/null +++ b/de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Configuration.java @@ -0,0 +1,41 @@ +/* © SRSoftware 2024 */ +package de.srsoftware.oidc.app; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.json.JSONObject; + +public class Configuration { + private final JSONObject json; + private final Path storageFile; + + public Configuration(File storage) throws IOException { + storageFile = storage.toPath(); + 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)); + } + + public String getOrDefault(String key, Object defaultValue) { + if (!json.has(key)) { + json.put(key, defaultValue.toString()); + save(); + } + return json.getString(key); + } + + public Configuration save() { + try { + Files.writeString(storageFile, json.toString(2)); + return this; + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStoreProvider.java b/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStoreProvider.java new file mode 100644 index 0000000..c957bcd --- /dev/null +++ b/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStoreProvider.java @@ -0,0 +1,28 @@ +/* © SRSoftware 2024 */ +package de.srsoftware.oidc.datastore.file; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; + + +public class FileStoreProvider extends HashMap { + private UuidHasher hasher; + + public FileStoreProvider(UuidHasher passwordHasher) { + hasher = passwordHasher; + } + + + @Override + public FileStore get(Object o) { + if (o instanceof File storageFile) try { + var fileStore = super.get(storageFile); + if (fileStore == null) put(storageFile, fileStore = new FileStore(storageFile, hasher)); + return fileStore; + } catch (IOException ioex) { + throw new RuntimeException(ioex); + } + return null; + } +} diff --git a/de.srsoftware.oidc.datastore.sqlite/build.gradle b/de.srsoftware.oidc.datastore.sqlite/build.gradle index 84b885c..8503eb7 100644 --- a/de.srsoftware.oidc.datastore.sqlite/build.gradle +++ b/de.srsoftware.oidc.datastore.sqlite/build.gradle @@ -16,6 +16,8 @@ dependencies { implementation project(':de.srsoftware.utils') implementation 'org.bitbucket.b_c:jose4j:0.9.6' implementation 'org.xerial:sqlite-jdbc:3.46.0.0' + implementation 'com.sun.mail:jakarta.mail:2.0.1' + } test { diff --git a/de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/ConnectionProvider.java b/de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/ConnectionProvider.java new file mode 100644 index 0000000..2928562 --- /dev/null +++ b/de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/ConnectionProvider.java @@ -0,0 +1,27 @@ +/* © SRSoftware 2024 */ +package de.srsoftware.oidc.datastore.sqlite; + +import java.io.File; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.HashMap; +import org.sqlite.SQLiteDataSource; + +public class ConnectionProvider extends HashMap { + public Connection get(Object o) { + if (o instanceof File dbFile) try { + var conn = super.get(dbFile); + if (conn == null) put(dbFile, conn = open(dbFile)); + return conn; + } catch (SQLException sqle) { + throw new RuntimeException(sqle); + } + return null; + } + + private Connection open(File dbFile) throws SQLException { + SQLiteDataSource dataSource = new SQLiteDataSource(); + dataSource.setUrl("jdbc:sqlite:%s".formatted(dbFile)); + return dataSource.getConnection(); + } +} diff --git a/de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteAuthService.java b/de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteAuthService.java new file mode 100644 index 0000000..a737d36 --- /dev/null +++ b/de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteAuthService.java @@ -0,0 +1,32 @@ +/* © SRSoftware 2024 */ +package de.srsoftware.oidc.datastore.sqlite; + +import de.srsoftware.oidc.api.AuthorizationService; +import de.srsoftware.oidc.api.data.AuthResult; +import de.srsoftware.oidc.api.data.Authorization; +import de.srsoftware.oidc.api.data.Client; +import de.srsoftware.oidc.api.data.User; +import java.sql.Connection; +import java.time.Instant; +import java.util.Collection; +import java.util.Optional; + +public class SqliteAuthService implements AuthorizationService { + public SqliteAuthService(Connection connection) { + } + + @Override + public AuthorizationService authorize(User user, Client client, Collection scopes, Instant expiration) { + return null; + } + + @Override + public Optional consumeAuthorization(String authCode) { + return Optional.empty(); + } + + @Override + public AuthResult getAuthorization(User user, Client client, Collection scopes) { + return null; + } +} diff --git a/de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteClientService.java b/de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteClientService.java new file mode 100644 index 0000000..3f60d43 --- /dev/null +++ b/de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteClientService.java @@ -0,0 +1,33 @@ +/* © SRSoftware 2024 */ +package de.srsoftware.oidc.datastore.sqlite; + +import de.srsoftware.oidc.api.ClientService; +import de.srsoftware.oidc.api.data.Client; +import java.sql.Connection; +import java.util.List; +import java.util.Optional; + +public class SqliteClientService implements ClientService { + public SqliteClientService(Connection connection) { + } + + @Override + public Optional getClient(String clientId) { + return Optional.empty(); + } + + @Override + public List listClients() { + return List.of(); + } + + @Override + public ClientService remove(Client client) { + return null; + } + + @Override + public ClientService save(Client client) { + return null; + } +} diff --git a/de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteMailConfig.java b/de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteMailConfig.java new file mode 100644 index 0000000..73bb417 --- /dev/null +++ b/de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteMailConfig.java @@ -0,0 +1,71 @@ +/* © SRSoftware 2024 */ +package de.srsoftware.oidc.datastore.sqlite; + +import de.srsoftware.oidc.api.MailConfig; +import jakarta.mail.Authenticator; +import java.sql.Connection; + +public class SqliteMailConfig implements MailConfig { + public SqliteMailConfig(Connection connection) { + } + + @Override + public String smtpHost() { + return ""; + } + + @Override + public MailConfig smtpHost(String newValue) { + return null; + } + + @Override + public int smtpPort() { + return 0; + } + + @Override + public MailConfig smtpPort(int newValue) { + return null; + } + + @Override + public String senderAddress() { + return ""; + } + + @Override + public MailConfig senderAddress(String newValue) { + return null; + } + + @Override + public String senderPassword() { + return ""; + } + + @Override + public MailConfig senderPassword(String newValue) { + return null; + } + + @Override + public MailConfig startTls(boolean newValue) { + return null; + } + + @Override + public MailConfig smtpAuth(boolean newValue) { + return null; + } + + @Override + public Authenticator authenticator() { + return null; + } + + @Override + public MailConfig save() { + return null; + } +} diff --git a/de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteSessionService.java b/de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteSessionService.java new file mode 100644 index 0000000..6f57356 --- /dev/null +++ b/de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteSessionService.java @@ -0,0 +1,39 @@ +/* © SRSoftware 2024 */ +package de.srsoftware.oidc.datastore.sqlite; + +import de.srsoftware.oidc.api.SessionService; +import de.srsoftware.oidc.api.data.Session; +import de.srsoftware.oidc.api.data.User; +import java.sql.Connection; +import java.time.Duration; +import java.util.Optional; + +public class SqliteSessionService implements SessionService { + public SqliteSessionService(Connection connection) { + } + + @Override + public Session createSession(User user) { + return null; + } + + @Override + public SessionService dropSession(String sessionId) { + return null; + } + + @Override + public Session extend(Session session) { + return null; + } + + @Override + public Optional retrieve(String sessionId) { + return Optional.empty(); + } + + @Override + public SessionService setDuration(Duration duration) { + return null; + } +} diff --git a/de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteUserService.java b/de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteUserService.java new file mode 100644 index 0000000..935801e --- /dev/null +++ b/de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteUserService.java @@ -0,0 +1,77 @@ +/* © SRSoftware 2024 */ +package de.srsoftware.oidc.datastore.sqlite; + +import de.srsoftware.oidc.api.UserService; +import de.srsoftware.oidc.api.data.AccessToken; +import de.srsoftware.oidc.api.data.User; +import java.sql.Connection; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +public class SqliteUserService implements UserService { + private final Connection conn; + + public SqliteUserService(Connection connection) { + conn = connection; + } + @Override + public AccessToken accessToken(User user) { + return null; + } + + @Override + public Optional consumeToken(String accessToken) { + return Optional.empty(); + } + + @Override + public UserService delete(User user) { + return null; + } + + @Override + public Optional forToken(String accessToken) { + return Optional.empty(); + } + + @Override + public UserService init(User defaultUser) { + return null; + } + + @Override + public List list() { + return List.of(); + } + + @Override + public Set find(String key) { + return Set.of(); + } + + @Override + public Optional load(String id) { + return Optional.empty(); + } + + @Override + public Optional load(String username, String password) { + return Optional.empty(); + } + + @Override + public boolean passwordMatches(String password, String hashedPassword) { + return false; + } + + @Override + public T save(User user) { + return null; + } + + @Override + public T updatePassword(User user, String plaintextPassword) { + return null; + } +} diff --git a/de.srsoftware.utils/src/main/java/de/srsoftware/utils/Paths.java b/de.srsoftware.utils/src/main/java/de/srsoftware/utils/Paths.java index 19940c1..34b3946 100644 --- a/de.srsoftware.utils/src/main/java/de/srsoftware/utils/Paths.java +++ b/de.srsoftware.utils/src/main/java/de/srsoftware/utils/Paths.java @@ -1,6 +1,7 @@ /* © SRSoftware 2024 */ package de.srsoftware.utils; +import java.io.File; import java.nio.file.Path; public class Paths { @@ -16,4 +17,9 @@ public class Paths { public static Path configDir(Object clazz) { return configDir(clazz.getClass()); } + + public static String extension(File file) { + var parts = file.getName().split("\\."); + return parts.length == 1 ? "" : parts[parts.length - 1]; + } }