Browse Source

dropped sqlite

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
devel
Stephan Richter 2 months ago
parent
commit
19fdfd059e
  1. 38
      Readme.md
  2. 2
      de.srsoftware.oidc.api/build.gradle
  3. 6
      de.srsoftware.oidc.app/build.gradle
  4. 114
      de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java
  5. 5
      de.srsoftware.oidc.datastore.encrypted/build.gradle
  6. 26
      de.srsoftware.oidc.datastore.sqlite/build.gradle
  7. 27
      de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/ConnectionProvider.java
  8. 170
      de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteAuthService.java
  9. 176
      de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteClientService.java
  10. 125
      de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteKeyStore.java
  11. 183
      de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteMailConfig.java
  12. 130
      de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteSessionService.java
  13. 20
      de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteStore.java
  14. 273
      de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteUserService.java
  15. 26
      de.srsoftware.oidc.datastore.sqlite/src/test/java/de/srsoftware/oidc/datastore/sqlite/SqliteAuthServiceTest.java
  16. 26
      de.srsoftware.oidc.datastore.sqlite/src/test/java/de/srsoftware/oidc/datastore/sqlite/SqliteClientServiceTest.java
  17. 26
      de.srsoftware.oidc.datastore.sqlite/src/test/java/de/srsoftware/oidc/datastore/sqlite/SqliteKeyStoreTest.java
  18. 37
      de.srsoftware.oidc.datastore.sqlite/src/test/java/de/srsoftware/oidc/datastore/sqlite/SqliteMailConfigTest.java
  19. 32
      de.srsoftware.oidc.datastore.sqlite/src/test/java/de/srsoftware/oidc/datastore/sqlite/SqliteSessionServiceTest.java
  20. 32
      de.srsoftware.oidc.datastore.sqlite/src/test/java/de/srsoftware/oidc/datastore/sqlite/SqliteUserServiceTest.java
  21. 3
      settings.gradle

38
Readme.md

@ -1,15 +1,49 @@
# LightOIDC # LightOIDC
<table>
<tr>
<th>Deutsch</th>
<th>English</th>
</tr>
<tr>
<td>
LightOIDC ist ein [Spezifikations][specification]-treuer OpenID-Connect-Provider mit minimalem Fußabdruck.
Dieses Ziel wird durch Minimierung der externen Abhängigkeiten auf ein absolutes Minimum erreicht:
Im Moment baut das Projekt nur auf die folgenden Laufzeit-Bibliotheken auf:
* [org.json:json](https://github.com/douglascrockford/JSON-java)
* [org.bitbucket.b_c:jose4j](https://bitbucket.org/b_c/jose4j)
* [com.sun.mail:jakarta.mail](https://projects.eclipse.org/projects/ee4j.mail)
* [com.sun.activation:jakarta.activation](https://projects.eclipse.org/projects/ee4j.jaf)
Im Moment haben diese Bibliotheken keine weiteren (transitiven) Anhängigkeiten, so dass das Projekt nicht durch eine Kaskade von Libraries aufgeblasen wird.
Das Ermöglicht es, dass die compilierte JAR-Datei weniger als 1,5 MB groß ist!
</td>
<td>
This aims to be a [specification] compliant OpenID connect provider with minimal footprint. This aims to be a [specification] compliant OpenID connect provider with minimal footprint.
This goal is achieved by reducing external library dependiencies to an absolute minimum. This goal is achieved by reducing external library dependiencies to an absolute minimum.
Currently, this project only depends on the following libraries: Currently, this project only depends on the following runtime libraries:
* [org.json:json](https://github.com/douglascrockford/JSON-java) * [org.json:json](https://github.com/douglascrockford/JSON-java)
* [org.bitbucket.b_c:jose4j](https://bitbucket.org/b_c/jose4j) * [org.bitbucket.b_c:jose4j](https://bitbucket.org/b_c/jose4j)
* [com.sun.mail:jakarta.mail](https://projects.eclipse.org/projects/ee4j.mail)
* [com.sun.activation:jakarta.activation](https://projects.eclipse.org/projects/ee4j.jaf)
At the time of writing, these libraries have no further transitive dependencies, thus mitigating any bloat from the project.
As a result, the compiled jar has a size of less than 1.5 MB!
At the time of writing, these libraries have no further transitive dependencies, this mitigating any bloat from the project. </td>
</tr>
</table>
## build ## build

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

@ -11,10 +11,10 @@ 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 project(':de.srsoftware.utils')
implementation 'org.json:json:20240303' implementation 'org.json:json:20240303'
implementation 'org.bitbucket.b_c:jose4j:0.9.6' implementation 'org.bitbucket.b_c:jose4j:0.9.6'
implementation 'com.sun.mail:jakarta.mail:2.0.1' implementation 'com.sun.mail:jakarta.mail:2.0.1'
implementation project(':de.srsoftware.utils')
} }
test { test {

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

@ -12,16 +12,14 @@ 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.xerial:sqlite-jdbc:3.46.0.0'
implementation project(':de.srsoftware.http') implementation project(':de.srsoftware.http')
implementation project(':de.srsoftware.logging') implementation project(':de.srsoftware.logging')
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.utils')
implementation project(':de.srsoftware.oidc.datastore.encrypted') implementation project(':de.srsoftware.oidc.datastore.encrypted')
implementation project(':de.srsoftware.oidc.datastore.file') implementation project(':de.srsoftware.oidc.datastore.file')
implementation project(':de.srsoftware.oidc.datastore.sqlite') implementation project(':de.srsoftware.oidc.web')
implementation project(':de.srsoftware.utils')
implementation 'org.json:json:20240303' implementation 'org.json:json:20240303'
} }

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

@ -6,7 +6,6 @@ import static de.srsoftware.oidc.api.Constants.*;
import static de.srsoftware.oidc.api.data.Permission.*; import static de.srsoftware.oidc.api.data.Permission.*;
import static de.srsoftware.utils.Optionals.emptyIfBlank; import static de.srsoftware.utils.Optionals.emptyIfBlank;
import static de.srsoftware.utils.Paths.configDir; import static de.srsoftware.utils.Paths.configDir;
import static de.srsoftware.utils.Paths.extension;
import static de.srsoftware.utils.Strings.uuid; import static de.srsoftware.utils.Strings.uuid;
import static java.lang.System.Logger.Level.DEBUG; import static java.lang.System.Logger.Level.DEBUG;
import static java.lang.System.Logger.Level.ERROR; import static java.lang.System.Logger.Level.ERROR;
@ -24,7 +23,6 @@ import de.srsoftware.oidc.datastore.encrypted.EncryptedMailConfig;
import de.srsoftware.oidc.datastore.encrypted.EncryptedUserService; import de.srsoftware.oidc.datastore.encrypted.EncryptedUserService;
import de.srsoftware.oidc.datastore.file.FileStoreProvider; import de.srsoftware.oidc.datastore.file.FileStoreProvider;
import de.srsoftware.oidc.datastore.file.PlaintextKeyStore; import de.srsoftware.oidc.datastore.file.PlaintextKeyStore;
import de.srsoftware.oidc.datastore.sqlite.*;
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 de.srsoftware.utils.UuidHasher; import de.srsoftware.utils.UuidHasher;
@ -47,12 +45,11 @@ public class Application {
public static final String ROOT = "/"; public static final String ROOT = "/";
public static final String STATIC_PATH = "/web"; public static final String STATIC_PATH = "/web";
private static final String BASE_PATH = "basePath"; private static final String BASE_PATH = "basePath";
private static final String FAVICON = "/favicon.ico"; private static final String FAVICON = "/favicon.ico";
private static final String INDEX = STATIC_PATH + "/index.html"; private static final String INDEX = STATIC_PATH + "/index.html";
private static final String WELL_KNOWN = "/.well-known"; private static final String WELL_KNOWN = "/.well-known";
private static System.Logger LOG = new ColorLogger("Application").setLogLevel(DEBUG); private static System.Logger LOG = new ColorLogger("Application").setLogLevel(DEBUG);
private static ConnectionProvider connectionProvider = new ConnectionProvider();
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
var argMap = map(args); var argMap = map(args);
@ -89,84 +86,63 @@ public class Application {
} }
private static ClientService setupClientService(Configuration config, Path defaultFile, FileStoreProvider fileStoreProvider) throws SQLException { private static ClientService setupClientService(Configuration config, Path defaultFile, FileStoreProvider fileStoreProvider) throws SQLException {
var clientStore = new File(config.getOrDefault("client_store", defaultFile)); var clientStore = new File(config.getOrDefault("client_store", defaultFile));
var clientService = switch (extension(clientStore)) { ClientService clientService = fileStoreProvider.get(clientStore);
case "db", "sqlite", "sqlite3" -> new SqliteClientService(connectionProvider.get(clientStore));
default -> fileStoreProvider.get(clientStore);
};
Optional<String> encryptionKey = config.get(ENCRYPTION_KEY); Optional<String> encryptionKey = config.get(ENCRYPTION_KEY);
if (encryptionKey.isPresent()){ if (encryptionKey.isPresent()) {
var salt = config.getOrDefault(SALT,uuid()); var salt = config.getOrDefault(SALT, uuid());
clientService = new EncryptedClientService(encryptionKey.get(),salt,clientService); clientService = new EncryptedClientService(encryptionKey.get(), salt, clientService);
} }
return clientService; return clientService;
} }
private static AuthorizationService setupAuthService(Configuration config, Path defaultFile, FileStoreProvider fileStoreProvider) throws SQLException { private static AuthorizationService setupAuthService(Configuration config, Path defaultFile, FileStoreProvider fileStoreProvider) throws SQLException {
var authServiceLocation = new File(config.getOrDefault("auth_store",defaultFile)); var authServiceLocation = new File(config.getOrDefault("auth_store", defaultFile));
return switch (extension(authServiceLocation)){ return fileStoreProvider.get(authServiceLocation);
case "db", "sqlite", "sqlite3" -> new SqliteAuthService(connectionProvider.get(authServiceLocation));
default -> fileStoreProvider.get(authServiceLocation);
};
} }
private static SessionService setupSessionService(Configuration config, Path defaultFile, FileStoreProvider fileStoreProvider) throws SQLException { private static SessionService setupSessionService(Configuration config, Path defaultFile, FileStoreProvider fileStoreProvider) {
var sessionStore = new File(config.getOrDefault("session_storage",defaultFile)); var sessionStore = new File(config.getOrDefault("session_storage", defaultFile));
return switch (extension(sessionStore)){ return fileStoreProvider.get(sessionStore);
case "db", "sqlite", "sqlite3" -> new SqliteSessionService(connectionProvider.get(sessionStore));
default -> fileStoreProvider.get(sessionStore);
};
} }
private static MailConfig setupMailConfig(Configuration config, Path defaultFile, FileStoreProvider fileStoreProvider) throws SQLException { private static MailConfig setupMailConfig(Configuration config, Path defaultFile, FileStoreProvider fileStoreProvider) throws SQLException {
var mailConfigLocation = new File(config.getOrDefault("mail_config_storage",defaultFile)); var mailConfigLocation = new File(config.getOrDefault("mail_config_storage", defaultFile));
var mailConfig = switch (extension(mailConfigLocation)){ MailConfig mailConfig = fileStoreProvider.get(mailConfigLocation);
case "db", "sqlite", "sqlite3" -> new SqliteMailConfig(connectionProvider.get(mailConfigLocation));
default -> fileStoreProvider.get(mailConfigLocation);
};
Optional<String> encryptionKey = config.get(ENCRYPTION_KEY); Optional<String> encryptionKey = config.get(ENCRYPTION_KEY);
if (encryptionKey.isPresent()){ if (encryptionKey.isPresent()) {
var salt = config.getOrDefault(SALT,uuid()); var salt = config.getOrDefault(SALT, uuid());
mailConfig = new EncryptedMailConfig(mailConfig,encryptionKey.get(),salt); mailConfig = new EncryptedMailConfig(mailConfig, encryptionKey.get(), salt);
} }
return mailConfig; return mailConfig;
} }
private static UserService setupUserService(Configuration config, Path defaultFile, FileStoreProvider fileStoreProvider, UuidHasher passHasher) throws SQLException { private static UserService setupUserService(Configuration config, Path defaultFile, FileStoreProvider fileStoreProvider, UuidHasher passHasher) throws SQLException {
var userStorageLocation = new File(config.getOrDefault("user_storage",defaultFile)); var userStorageLocation = new File(config.getOrDefault("user_storage", defaultFile));
var userService = switch (extension(userStorageLocation).toLowerCase()){ UserService userService = fileStoreProvider.get(userStorageLocation);
case "db", "sqlite", "sqlite3" -> new SqliteUserService(connectionProvider.get(userStorageLocation),passHasher);
default -> fileStoreProvider.get(userStorageLocation);
};
Optional<String> encryptionKey = config.get(ENCRYPTION_KEY); Optional<String> encryptionKey = config.get(ENCRYPTION_KEY);
if (encryptionKey.isPresent()){ if (encryptionKey.isPresent()) {
var salt = config.getOrDefault(SALT,uuid()); var salt = config.getOrDefault(SALT, uuid());
userService = new EncryptedUserService(userService,encryptionKey.get(),salt,passHasher); userService = new EncryptedUserService(userService, encryptionKey.get(), salt, passHasher);
} }
return userService; return userService;
} }
private static KeyStorage setupKeyStore(Configuration config, Path defaultConfigDir) throws SQLException { private static KeyStorage setupKeyStore(Configuration config, Path defaultConfigDir) throws SQLException {
var keyStorageLocation = new File(config.getOrDefault("key_storage", defaultConfigDir.resolve("keys"))); var keyStorageLocation = new File(config.getOrDefault("key_storage", defaultConfigDir.resolve("keys")));
KeyStorage keyStore = null; KeyStorage keyStore = new PlaintextKeyStore(keyStorageLocation.toPath());
if ((keyStorageLocation.exists() && keyStorageLocation.isDirectory()) || !keyStorageLocation.getName().contains(".")) {
keyStore = new PlaintextKeyStore(keyStorageLocation.toPath());
} else { // SQLite
var conn = connectionProvider.get(keyStorageLocation);
keyStore = new SqliteKeyStore(conn);
}
Optional<String> encryptionKey = config.get(ENCRYPTION_KEY); Optional<String> encryptionKey = config.get(ENCRYPTION_KEY);
if (encryptionKey.isPresent()){ if (encryptionKey.isPresent()) {
var salt = config.getOrDefault(SALT,uuid()); var salt = config.getOrDefault(SALT, uuid());
keyStore = new EncryptedKeyStore(encryptionKey.get(),salt,keyStore); keyStore = new EncryptedKeyStore(encryptionKey.get(), salt, keyStore);
} }
return keyStore; return keyStore;
} }
@ -183,18 +159,18 @@ public class Application {
var token = tokens.remove(0); var token = tokens.remove(0);
switch (token) { switch (token) {
case "--base": case "--base":
if (tokens.isEmpty()) throw new IllegalArgumentException("--base option requires second argument!"); if (tokens.isEmpty()) throw new IllegalArgumentException("--base option requires second argument!");
map.put(BASE_PATH, Path.of(tokens.remove(0))); map.put(BASE_PATH, Path.of(tokens.remove(0)));
break; break;
case "--config": case "--config":
if (tokens.isEmpty()) throw new IllegalArgumentException("--config option requires second argument!"); if (tokens.isEmpty()) throw new IllegalArgumentException("--config option requires second argument!");
map.put(CONFIG_PATH, Path.of(tokens.remove(0))); map.put(CONFIG_PATH, Path.of(tokens.remove(0)));
break; break;
default: default:
LOG.log(ERROR, "Unknown option: {0}", token); LOG.log(ERROR, "Unknown option: {0}", token);
}
} }
}
return map; return map;
} }
} }

5
de.srsoftware.oidc.datastore.encrypted/build.gradle

@ -12,12 +12,11 @@ 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'
testImplementation project(path: ':de.srsoftware.oidc.api', configuration: "testBundle")
implementation project(':de.srsoftware.oidc.api') implementation project(':de.srsoftware.oidc.api')
implementation 'com.sun.mail:jakarta.mail:2.0.1'
implementation project(':de.srsoftware.utils') implementation project(':de.srsoftware.utils')
testImplementation project(path: ':de.srsoftware.oidc.api', configuration: "testBundle") implementation 'com.sun.mail:jakarta.mail:2.0.1'
implementation 'org.bitbucket.b_c:jose4j:0.9.6' implementation 'org.bitbucket.b_c:jose4j:0.9.6'
} }
test { test {

26
de.srsoftware.oidc.datastore.sqlite/build.gradle

@ -1,26 +0,0 @@
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'
testImplementation project(path: ':de.srsoftware.oidc.api', configuration: "testBundle")
implementation project(':de.srsoftware.oidc.api')
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 {
useJUnitPlatform()
}

27
de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/ConnectionProvider.java

@ -1,27 +0,0 @@
/* © 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<File, Connection> {
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();
}
}

170
de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteAuthService.java

@ -1,170 +0,0 @@
/* © SRSoftware 2024 */
package de.srsoftware.oidc.datastore.sqlite;
import static de.srsoftware.utils.Optionals.nullable;
import static de.srsoftware.utils.Strings.uuid;
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.AuthorizedScopes;
import java.sql.Connection;
import java.sql.SQLException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
public class SqliteAuthService extends SqliteStore implements AuthorizationService {
private static final String STORE_VERSION = "auth_store_version";
private static final String CREATE_STORE_VERSION = "INSERT INTO metainfo (key,value) VALUES ('" + STORE_VERSION + "','0')";
private static final String SELECT_STORE_VERSION = "SELECT * FROM metainfo WHERE key = '" + STORE_VERSION + "'";
private static final String SET_STORE_VERSION = "UPDATE metainfo SET value = ? WHERE key = '" + STORE_VERSION + "'";
private static final String CREATE_AUTHSTORE_TABLE = "CREATE TABLE IF NOT EXISTS authorizations(userId VARCHAR(255), clientId VARCHAR(255), scope VARCHAR(255), expiration LONG, PRIMARY KEY(userId, clientId, scope));";
private static final String SAVE_AUTHORIZATION = "INSERT INTO authorizations(userId, clientId, scope, expiration) VALUES (?,?,?,?) ON CONFLICT DO UPDATE SET expiration = ?";
private static final String SELECT_AUTH = "SELECT * FROM authorizations WHERE userId = ? AND clientId = ? AND scope IN";
private static final String SELECT_USER_CLIENTS = "SELECT DISTINCT clientId FROM authorizations WHERE userId = ?";
private Map<String, Authorization> authCodes = new HashMap<>();
private Map<String, String> nonceMap = new HashMap<>();
public SqliteAuthService(Connection connection) throws SQLException {
super(connection);
}
private void createStoreTables() throws SQLException {
conn.prepareStatement(CREATE_AUTHSTORE_TABLE).execute();
}
@Override
protected void initTables() throws SQLException {
var rs = conn.prepareStatement(SELECT_STORE_VERSION).executeQuery();
int availableVersion = 1;
int currentVersion;
if (rs.next()) {
currentVersion = rs.getInt("value");
rs.close();
} else {
rs.close();
conn.prepareStatement(CREATE_STORE_VERSION).execute();
currentVersion = 0;
}
conn.setAutoCommit(false);
var stmt = conn.prepareStatement(SET_STORE_VERSION);
while (currentVersion < availableVersion) {
try {
switch (currentVersion) {
case 0:
createStoreTables();
break;
}
stmt.setInt(1, ++currentVersion);
stmt.execute();
conn.commit();
} catch (Exception e) {
conn.rollback();
LOG.log(System.Logger.Level.ERROR, "Failed to update at {} = {}", STORE_VERSION, currentVersion);
break;
}
}
conn.setAutoCommit(true);
}
private String authCode(Authorization authorization) {
var code = uuid();
authCodes.put(code, authorization);
return code;
}
@Override
public AuthorizationService authorize(String userId, String clientId, Collection<String> scopes, Instant expiration) {
try {
conn.setAutoCommit(false);
var stmt = conn.prepareStatement(SAVE_AUTHORIZATION);
stmt.setString(1, userId);
stmt.setString(2, clientId);
stmt.setLong(4, expiration.getEpochSecond());
stmt.setLong(5, expiration.getEpochSecond());
for (var scope : scopes) {
stmt.setString(3, scope);
stmt.execute();
}
conn.commit();
conn.setAutoCommit(true);
return this;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public List<String> authorizedClients(String userId) {
try {
var stmt = conn.prepareStatement(SELECT_USER_CLIENTS);
stmt.setString(1, userId);
var rs = stmt.executeQuery();
var result = new ArrayList<String>();
while (rs.next()) result.add(rs.getString(1));
rs.close();
return result;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public Optional<Authorization> consumeAuthorization(String authCode) {
return nullable(authCodes.remove(authCode));
}
@Override
public Optional<String> consumeNonce(String userId, String clientId) {
var nonceKey = String.join("@", userId, clientId);
return nullable(nonceMap.get(nonceKey));
}
@Override
public AuthResult getAuthorization(String userId, String clientId, Collection<String> scopes) {
try {
var scopeList = "(" + scopes.stream().map(s -> "?").collect(Collectors.joining(", ")) + ")";
var sql = SELECT_AUTH + scopeList;
var stmt = conn.prepareStatement(sql);
stmt.setString(1, userId);
stmt.setString(2, clientId);
int i = 3;
for (var scope : scopes) stmt.setString(i++, scope);
var rs = stmt.executeQuery();
var unauthorized = new HashSet<String>(scopes);
var authorized = new HashSet<String>();
var now = Instant.now();
Instant earliestExp = null;
while (rs.next()) {
long expiration = rs.getLong("expiration");
String scope = rs.getString("scope");
Instant ex = Instant.ofEpochSecond(expiration).truncatedTo(ChronoUnit.SECONDS);
if (ex.isAfter(now)) {
unauthorized.remove(scope);
authorized.add(scope);
if (earliestExp == null || ex.isBefore(earliestExp)) earliestExp = ex;
}
}
rs.close();
if (authorized.isEmpty()) return new AuthResult(null, unauthorized, null);
var authorizedScopes = new AuthorizedScopes(authorized, earliestExp);
var authorization = new Authorization(clientId, userId, authorizedScopes);
return new AuthResult(authorizedScopes, unauthorized, authCode(authorization));
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public void nonce(String userId, String clientId, String nonce) {
var nonceKey = String.join("@", userId, clientId);
if (nonce != null) {
nonceMap.put(nonceKey, nonce);
} else
nonceMap.remove(nonceKey);
}
}

176
de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteClientService.java

@ -1,176 +0,0 @@
/* © SRSoftware 2024 */
package de.srsoftware.oidc.datastore.sqlite;
import static de.srsoftware.oidc.api.Constants.*;
import de.srsoftware.oidc.api.ClientService;
import de.srsoftware.oidc.api.data.Client;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.*;
import java.util.stream.Collectors;
public class SqliteClientService extends SqliteStore implements ClientService {
private static final String STORE_VERSION = "client_store_version";
private static final String CREATE_STORE_VERSION = "INSERT INTO metainfo (key,value) VALUES ('" + STORE_VERSION + "','0')";
private static final String SELECT_STORE_VERSION = "SELECT * FROM metainfo WHERE key = '" + STORE_VERSION + "'";
private static final String SET_STORE_VERSION = "UPDATE metainfo SET value = ? WHERE key = '" + STORE_VERSION + "'";
private static final String CREATE_CLIENT_TABLE = "CREATE TABLE IF NOT EXISTS clients(id VARCHAR(255) NOT NULL PRIMARY KEY, name VARCHAR(255), secret VARCHAR(255), landing_page VARCHAR(255));";
private static final String CREATE_REDIRECT_TABLE = "CREATE TABLE IF NOT EXISTS client_redirects(clientId VARCHAR(255), uri VARCHAR(255), PRIMARY KEY(clientId, uri));";
private static final String SAVE_CLIENT = "INSERT INTO clients (id, name, secret, landing_page) VALUES (?,?,?,?) ON CONFLICT DO UPDATE SET name = ?, secret = ?, landing_page = ?;";
private static final String SAVE_REDIRECT = "INSERT OR IGNORE INTO client_redirects(clientId, uri) VALUES (?, ?)";
private static final String DROP_OTHER_REDIRECTS = "DELETE FROM client_redirects WHERE clientId = ? AND uri NOT IN";
private static final String SELECT_CLIENT = "SELECT * FROM clients WHERE id = ?";
private static final String SELECT_CLIENT_REDIRECTS = "SELECT uri FROM client_redirects WHERE clientId = ?";
private static final String LIST_CLIENT_REDIRECTS = "SELECT * FROM client_redirects";
private static final String LIST_CLIENTS = "SELECT * FROM clients";
private static final String DELETE_CLIENT = "DELETE FROM clients WHERE id = ?";
private static final String DELETE_CLIENT_REDIRECTS = "DELETE FROM client_redirects WHERE clientId = ?";
public SqliteClientService(Connection connection) throws SQLException {
super(connection);
}
private void createStoreTables() throws SQLException {
conn.prepareStatement(CREATE_CLIENT_TABLE).execute();
conn.prepareStatement(CREATE_REDIRECT_TABLE).execute();
}
@Override
protected void initTables() throws SQLException {
var rs = conn.prepareStatement(SELECT_STORE_VERSION).executeQuery();
int availableVersion = 1;
int currentVersion;
if (rs.next()) {
currentVersion = rs.getInt("value");
rs.close();
} else {
rs.close();
conn.prepareStatement(CREATE_STORE_VERSION).execute();
currentVersion = 0;
}
conn.setAutoCommit(false);
var stmt = conn.prepareStatement(SET_STORE_VERSION);
while (currentVersion < availableVersion) {
try {
switch (currentVersion) {
case 0:
createStoreTables();
break;
}
stmt.setInt(1, ++currentVersion);
stmt.execute();
conn.commit();
} catch (Exception e) {
conn.rollback();
LOG.log(System.Logger.Level.ERROR, "Failed to update at {} = {}", STORE_VERSION, currentVersion);
break;
}
}
conn.setAutoCommit(true);
}
@Override
public Optional<Client> getClient(String clientId) {
Optional<Client> result = Optional.empty();
try {
var stmt = conn.prepareStatement(SELECT_CLIENT_REDIRECTS);
stmt.setString(1, clientId);
var rs = stmt.executeQuery();
var uris = new HashSet<String>();
while (rs.next()) uris.add(rs.getString("uri"));
rs.close();
stmt = conn.prepareStatement(SELECT_CLIENT);
stmt.setString(1, clientId);
rs = stmt.executeQuery();
if (rs.next()) {
var name = rs.getString(NAME);
var secret = rs.getString(SECRET);
var landing = rs.getString(LANDING_PAGE);
result = Optional.of(new Client(clientId, name, secret, uris).landingPage(landing));
}
rs.close();
return result;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public List<Client> listClients() {
try {
var stmt = conn.prepareStatement(LIST_CLIENT_REDIRECTS);
var rs = stmt.executeQuery();
var redirects = new HashMap<String, Set<String>>();
while (rs.next()) {
var clientId = rs.getString("clientId");
var uri = rs.getString("uri");
var set = redirects.computeIfAbsent(clientId, k -> new HashSet<>());
set.add(uri);
}
rs.close();
stmt = conn.prepareStatement(LIST_CLIENTS);
rs = stmt.executeQuery();
var result = new ArrayList<Client>();
while (rs.next()) {
var id = rs.getString("id");
var name = rs.getString(NAME);
var secret = rs.getString(SECRET);
var landing = rs.getString(LANDING_PAGE);
result.add(new Client(id, name, secret, redirects.get(id)).landingPage(landing));
}
rs.close();
return result;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public ClientService remove(String clientId) {
try {
var stmt = conn.prepareStatement(DELETE_CLIENT);
stmt.setString(1, clientId);
stmt.execute();
stmt = conn.prepareStatement(DELETE_CLIENT_REDIRECTS);
stmt.setString(1, clientId);
stmt.execute();
return this;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public ClientService save(Client client) {
try {
var stmt = conn.prepareStatement(SAVE_CLIENT);
stmt.setString(1, client.id());
stmt.setString(2, client.name());
stmt.setString(3, client.secret());
stmt.setString(4, client.landingPage());
stmt.setString(5, client.name());
stmt.setString(6, client.secret());
stmt.setString(7, client.landingPage());
stmt.execute();
stmt = conn.prepareStatement(SAVE_REDIRECT);
stmt.setString(1, client.id());
for (var redirect : client.redirectUris()) {
stmt.setString(2, redirect);
stmt.execute();
}
var where = "(" + client.redirectUris().stream().map(u -> "?").collect(Collectors.joining(", ")) + ")";
var sql = DROP_OTHER_REDIRECTS + where;
stmt = conn.prepareStatement(sql);
stmt.setString(1, client.id());
int i = 2;
for (var redirect : client.redirectUris()) stmt.setString(i++, redirect);
stmt.execute();
return this;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}

125
de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteKeyStore.java

@ -1,125 +0,0 @@
/* © SRSoftware 2024 */
package de.srsoftware.oidc.datastore.sqlite;
import de.srsoftware.oidc.api.KeyStorage;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import org.jose4j.jwk.PublicJsonWebKey;
public class SqliteKeyStore extends SqliteStore implements KeyStorage {
private static final String STORE_VERSION = "key_store_version";
private static final String CREATE_STORE_VERSION = "INSERT INTO metainfo (key,value) VALUES ('" + STORE_VERSION + "','0')";
private static final String SELECT_STORE_VERSION = "SELECT * FROM metainfo WHERE key = '" + STORE_VERSION + "'";
private static final String SET_STORE_VERSION = "UPDATE metainfo SET value = ? WHERE key = '" + STORE_VERSION + "'";
private static final String SET_KEYSTORE_VERSION = "UPDATE metainfo SET value = ? WHERE key = 'key_store_version'";
private static final String CREATE_KEYSTORE_TABLE = "CREATE TABLE IF NOT EXISTS keystore(key_id VARCHAR(255) PRIMARY KEY, json TEXT NOT NULL);";
private static final String SAVE_KEY = "INSERT INTO keystore(key_id, json) values (?,?) ON CONFLICT(key_id) DO UPDATE SET json = ?";
private static final String SELECT_KEY_IDS = "SELECT key_id FROM keystore";
private static final String LOAD_KEY = "SELECT json FROM keystore WHERE key_id = ?";
private static final String DROP_KEY = "DELETE FROM keystore WHERE key_id = ?";
private HashMap<String, PublicJsonWebKey> loaded = new HashMap<>();
public SqliteKeyStore(Connection connection) throws SQLException {
super(connection);
}
private void createStoreTables() throws SQLException {
conn.prepareStatement(CREATE_KEYSTORE_TABLE).execute();
}
@Override
public KeyStorage drop(String keyId) {
try {
var stmt = conn.prepareStatement(DROP_KEY);
stmt.setString(1, keyId);
stmt.execute();
} catch (SQLException e) {
LOG.log(System.Logger.Level.WARNING, "Failed to drop key {0} from database:", keyId, e);
}
return this;
}
@Override
protected void initTables() throws SQLException {
var rs = conn.prepareStatement(SELECT_STORE_VERSION).executeQuery();
int availableVersion = 1;
int currentVersion;
if (rs.next()) {
currentVersion = rs.getInt("value");
rs.close();
} else {
rs.close();
conn.prepareStatement(CREATE_STORE_VERSION).execute();
currentVersion = 0;
}
conn.setAutoCommit(false);
var stmt = conn.prepareStatement(SET_STORE_VERSION);
while (currentVersion < availableVersion) {
try {
switch (currentVersion) {
case 0:
createStoreTables();
break;
}
stmt.setInt(1, ++currentVersion);
stmt.execute();
conn.commit();
} catch (Exception e) {
conn.rollback();
LOG.log(System.Logger.Level.ERROR, "Failed to update at {} = {}", STORE_VERSION, currentVersion);
break;
}
}
conn.setAutoCommit(true);
}
@Override
public List<String> listKeys() {
var result = new ArrayList<String>();
try {
var rs = conn.prepareStatement(SELECT_KEY_IDS).executeQuery();
while (rs.next()) result.add(rs.getString(1));
rs.close();
} catch (SQLException e) {
LOG.log(System.Logger.Level.WARNING, "Failed to read key ids from table!");
}
return result;
}
@Override
public String loadJson(String keyId) throws IOException {
try {
var stmt = conn.prepareStatement(LOAD_KEY);
stmt.setString(1, keyId);
var rs = stmt.executeQuery();
String json = null;
if (rs.next()) json = rs.getString(1);
rs.close();
return json;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public KeyStorage store(String keyId, String json) throws IOException {
try {
var stmt = conn.prepareStatement(SAVE_KEY);
stmt.setString(1, keyId);
stmt.setString(2, json);
stmt.setString(3, json);
stmt.execute();
} catch (SQLException e) {
throw new RuntimeException(e);
}
return this;
}
}

183
de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteMailConfig.java

@ -1,183 +0,0 @@
/* © SRSoftware 2024 */
package de.srsoftware.oidc.datastore.sqlite;
import static de.srsoftware.oidc.api.Constants.*;
import de.srsoftware.oidc.api.MailConfig;
import jakarta.mail.Authenticator;
import jakarta.mail.PasswordAuthentication;
import java.sql.Connection;
import java.sql.SQLException;
public class SqliteMailConfig extends SqliteStore implements MailConfig {
private static final String STORE_VERSION = "mail_config_store_version";
private static final String CREATE_STORE_VERSION = "INSERT INTO metainfo (key,value) VALUES ('" + STORE_VERSION + "','0')";
private static final String SELECT_STORE_VERSION = "SELECT * FROM metainfo WHERE key = '" + STORE_VERSION + "'";
private static final String SET_STORE_VERSION = "UPDATE metainfo SET value = ? WHERE key = '" + STORE_VERSION + "'";
private static final String CREATE_MAIL_CONFIG_TABLE = "CREATE TABLE mail_config (key VARCHAR(64) PRIMARY KEY, value VARCHAR(255));";
private static final String SAVE_MAILCONFIG = "INSERT INTO mail_config (key, value) VALUES (?, ?) ON CONFLICT DO UPDATE SET value = ?";
private static final String SELECT_MAILCONFIG = "SELECT * FROM mail_config";
private String smtpHost, senderAddress, password;
private int smtpPort;
private boolean smtpAuth, startTls;
private Authenticator auth;
public SqliteMailConfig(Connection connection) throws SQLException {
super(connection);
smtpHost = "";
smtpPort = 0;
senderAddress = "";
password = "";
smtpAuth = true;
startTls = true;
var rs = conn.prepareStatement(SELECT_MAILCONFIG).executeQuery();
while (rs.next()) {
var key = rs.getString(1);
switch (key) {
case SMTP_PORT -> smtpPort = rs.getInt(2);
case SMTP_HOST -> smtpHost = rs.getString(2);
case START_TLS -> startTls = rs.getBoolean(2);
case SMTP_AUTH -> smtpAuth = rs.getBoolean(2);
case SMTP_PASSWORD -> password = rs.getString(2);
case SMTP_USER -> senderAddress = rs.getString(2);
}
}
}
private void createStoreTables() throws SQLException {
conn.prepareStatement(CREATE_MAIL_CONFIG_TABLE).execute();
}
@Override
protected void initTables() throws SQLException {
var rs = conn.prepareStatement(SELECT_STORE_VERSION).executeQuery();
int availableVersion = 1;
int currentVersion;
if (rs.next()) {
currentVersion = rs.getInt("value");
rs.close();
} else {
rs.close();
conn.prepareStatement(CREATE_STORE_VERSION).execute();
currentVersion = 0;
}
conn.setAutoCommit(false);
var stmt = conn.prepareStatement(SET_STORE_VERSION);
while (currentVersion < availableVersion) {
try {
switch (currentVersion) {
case 0:
createStoreTables();
break;
}
stmt.setInt(1, ++currentVersion);
stmt.execute();
conn.commit();
}
catch (Exception e) {
conn.rollback();
LOG.log(System.Logger.Level.ERROR, "Failed to update at {} = {}", STORE_VERSION, currentVersion);
break;
}
}
conn.setAutoCommit(true);
}
@Override
public String smtpHost() {
return smtpHost;
}
@Override
public MailConfig smtpHost(String newValue) {
smtpHost = newValue;
return this;
}
@Override
public int smtpPort() {
return smtpPort;
}
@Override
public MailConfig smtpPort(int newValue) {
smtpPort = newValue;
return this;
}
@Override
public String senderAddress() {
return senderAddress;
}
@Override
public MailConfig senderAddress(String newValue) {
senderAddress = newValue;
return this;
}
@Override
public String senderPassword() {
return password;
}
@Override
public MailConfig senderPassword(String newValue) {
password = newValue;
return this;
}
@Override
public boolean startTls() {
return startTls;
}
@Override
public boolean smtpAuth() {
return smtpAuth;
}
@Override
public MailConfig startTls(boolean newValue) {
startTls = newValue;
return this;
}
@Override
public MailConfig smtpAuth(boolean newValue) {
smtpAuth = newValue;
return this;
}
@Override
public Authenticator authenticator() {
if (auth == null) {
auth = new Authenticator() {
// override the getPasswordAuthentication method
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(senderAddress(), senderPassword());
}
};
}
return auth;
}
@Override
public MailConfig save() {
try {
var stmt = conn.prepareStatement(SAVE_MAILCONFIG);
for (var entry : map().entrySet()) {
stmt.setString(1, entry.getKey());
stmt.setObject(2, entry.getValue());
stmt.setObject(3, entry.getValue());
stmt.execute();
}
return this;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}

130
de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteSessionService.java

@ -1,130 +0,0 @@
/* © SRSoftware 2024 */
package de.srsoftware.oidc.datastore.sqlite;
import static de.srsoftware.utils.Strings.uuid;
import static java.time.temporal.ChronoUnit.SECONDS;
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.sql.SQLException;
import java.time.Instant;
import java.util.Optional;
public class SqliteSessionService extends SqliteStore implements SessionService {
private static final String STORE_VERSION = "session_store_version";
private static final String CREATE_STORE_VERSION = "INSERT INTO metainfo (key,value) VALUES ('" + STORE_VERSION + "','0')";
private static final String SELECT_STORE_VERSION = "SELECT * FROM metainfo WHERE key = '" + STORE_VERSION + "'";
private static final String SET_STORE_VERSION = "UPDATE metainfo SET value = ? WHERE key = '" + STORE_VERSION + "'";
private static final String CREATE_SESSION_TABLE = "CREATE TABLE sessions (id VARCHAR(64) PRIMARY KEY, userId VARCHAR(64) NOT NULL, expiration LONG NOT NULL, trust_browser BOOLEAN DEFAULT false)";
private static final String SAVE_SESSION = "INSERT INTO sessions (id, userId, expiration, trust_browser) VALUES (?,?,?, ?) ON CONFLICT DO UPDATE SET expiration = ?, trust_browser = ?;";
private static final String DROP_SESSION = "DELETE FROM sessions WHERE id = ?";
private static final String SELECT_SESSION = "SELECT * FROM sessions WHERE id = ?";
public SqliteSessionService(Connection connection) throws SQLException {
super(connection);
}
@Override
public Session createSession(User user, boolean trustBrowser) {
var now = Instant.now();
var endOfSession = now.plus(user.sessionDuration()).truncatedTo(SECONDS);
return save(new Session(user.uuid(), endOfSession, uuid(), trustBrowser));
}
private void createStoreTables() throws SQLException {
conn.prepareStatement(CREATE_SESSION_TABLE).execute();
}
@Override
public SessionService dropSession(String sessionId) {
try {
var stmt = conn.prepareStatement(DROP_SESSION);
stmt.setString(1, sessionId);
stmt.execute();
} catch (SQLException e) {
throw new RuntimeException(e);
}
return this;
}
@Override
public Session extend(Session session, User user) {
var endOfSession = Instant.now().plus(user.sessionDuration());
return save(new Session(user.uuid(), endOfSession, session.id(), session.trustBrowser()));
}
@Override
protected void initTables() throws SQLException {
var rs = conn.prepareStatement(SELECT_STORE_VERSION).executeQuery();
int availableVersion = 1;
int currentVersion;
if (rs.next()) {
currentVersion = rs.getInt("value");
rs.close();
} else {
rs.close();
conn.prepareStatement(CREATE_STORE_VERSION).execute();
currentVersion = 0;
}
conn.setAutoCommit(false);
var stmt = conn.prepareStatement(SET_STORE_VERSION);
while (currentVersion < availableVersion) {
try {
switch (currentVersion) {
case 0:
createStoreTables();
break;
}
stmt.setInt(1, ++currentVersion);
stmt.execute();
conn.commit();
} catch (Exception e) {
conn.rollback();
LOG.log(System.Logger.Level.ERROR, "Failed to update at {} = {}", STORE_VERSION, currentVersion);
break;
}
}
conn.setAutoCommit(true);
}
@Override
public Optional<Session> retrieve(String sessionId) {
try {
var stmt = conn.prepareStatement(SELECT_SESSION);
stmt.setString(1, sessionId);
var rs = stmt.executeQuery();
Optional<Session> result = Optional.empty();
if (rs.next()) {
var userID = rs.getString("userId");
var expiration = Instant.ofEpochSecond(rs.getLong("expiration"));
var trustBrowser = rs.getBoolean("trust_browser");
if (expiration.isAfter(Instant.now())) result = Optional.of(new Session(userID, expiration, sessionId, trustBrowser));
}
rs.close();
return result;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private Session save(Session session) {
try {
var stmt = conn.prepareStatement(SAVE_SESSION);
var expiration = session.expiration().getEpochSecond();
stmt.setString(1, session.id());
stmt.setString(2, session.userId());
stmt.setLong(3, expiration);
stmt.setBoolean(4, session.trustBrowser());
stmt.setLong(5, expiration);
stmt.setBoolean(6, session.trustBrowser());
stmt.execute();
return session;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}

20
de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteStore.java

@ -1,20 +0,0 @@
/* © SRSoftware 2024 */
package de.srsoftware.oidc.datastore.sqlite;
import java.sql.Connection;
import java.sql.SQLException;
public abstract class SqliteStore {
public static System.Logger LOG = System.getLogger(SqliteStore.class.getSimpleName());
private static final String CREATE_MIGRATION_TABLE = "CREATE TABLE IF NOT EXISTS metainfo(key VARCHAR(255) PRIMARY KEY, value TEXT);";
protected final Connection conn;
public SqliteStore(Connection connection) throws SQLException {
conn = connection;
conn.prepareStatement(CREATE_MIGRATION_TABLE).execute();
initTables();
}
protected abstract void initTables() throws SQLException;
}

273
de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteUserService.java

@ -1,273 +0,0 @@
/* © SRSoftware 2024 */
package de.srsoftware.oidc.datastore.sqlite;
import static de.srsoftware.utils.Optionals.nullable;
import static de.srsoftware.utils.Strings.uuid;
import static java.util.Optional.empty;
import de.srsoftware.oidc.api.UserService;
import de.srsoftware.oidc.api.data.AccessToken;
import de.srsoftware.oidc.api.data.Permission;
import de.srsoftware.oidc.api.data.User;
import de.srsoftware.utils.PasswordHasher;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
public class SqliteUserService extends SqliteStore implements UserService {
private static final String STORE_VERSION = "user_store_version";
private static final String CREATE_STORE_VERSION = "INSERT INTO metainfo (key,value) VALUES ('" + STORE_VERSION + "','0')";
private static final String SELECT_STORE_VERSION = "SELECT * FROM metainfo WHERE key = '" + STORE_VERSION + "'";
private static final String SET_STORE_VERSION = "UPDATE metainfo SET value = ? WHERE key = '" + STORE_VERSION + "'";
private static final String CREATE_USER_TABLE = "CREATE TABLE IF NOT EXISTS users(uuid VARCHAR(255) NOT NULL PRIMARY KEY, password VARCHAR(255), email VARCHAR(255), session_duration INT NOT NULL DEFAULT 10, username VARCHAR(255), realname VARCHAR(255));";
private static final String CREATE_USER_PERMISSION_TABLE = "CREATE TABLE IF NOT EXISTS user_permissions(uuid VARCHAR(255), permission VARCHAR(50), PRIMARY KEY(uuid,permission));";
private static final String COUNT_USERS = "SELECT count(*) FROM users";
private static final String LOAD_USER = "SELECT * FROM users WHERE uuid = ?";
private static final String LOAD_PERMISSIONS = "SELECT permission FROM user_permissions WHERE uuid = ?";
private static final String FIND_USER = "SELECT * FROM users WHERE uuid = ? OR username LIKE ? OR realname LIKE ? OR email = ? ORDER BY COALESCE(uuid, ?), username";
private static final String LIST_USERS = "SELECT * FROM users";
private static final String SAVE_USER = "INSERT INTO users (uuid,password,email,session_duration,username,realname) VALUES (?,?,?,?,?,?) ON CONFLICT DO UPDATE SET password = ?, email = ?, session_duration = ?, username = ?, realname = ?;";
private static final String INSERT_PERMISSIONS = "INSERT INTO user_permissions (uuid, permission) VALUES (?,?)";
private static final String DROP_PERMISSIONS = "DELETE FROM user_permissions WHERE uuid = ?";
private static final String DROP_USER = "DELETE FROM users WHERE uuid = ?";
private static final String UPDATE_PASSWORD = "UPDATE users SET password = ? WHERE uuid = ?";
private final PasswordHasher<String> hasher;
private Map<String, AccessToken> accessTokens = new HashMap<>();
public SqliteUserService(Connection connection, PasswordHasher<String> passHasher) throws SQLException {
super(connection);
hasher = passHasher;
}
@Override
public AccessToken accessToken(User user) {
var token = new AccessToken(uuid(), Objects.requireNonNull(user), Instant.now().plus(1, ChronoUnit.HOURS));
accessTokens.put(token.id(), token);
return token;
}
private User addPermissions(User user) {
try {
var stmt = conn.prepareStatement(LOAD_PERMISSIONS);
stmt.setString(1, user.uuid());
var rs = stmt.executeQuery();
while (rs.next()) try {
user.add(Permission.valueOf(rs.getString("permission")));
} catch (IllegalArgumentException ignored) {
}
rs.close();
return user;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public Optional<User> consumeToken(String id) {
var user = forToken(id);
accessTokens.remove(id);
return user;
}
private void createStoreTables() throws SQLException {
conn.prepareStatement(CREATE_USER_TABLE).execute();
conn.prepareStatement(CREATE_USER_PERMISSION_TABLE).execute();
}
@Override
public UserService delete(User user) {
try {
conn.setAutoCommit(false);
dropPermissionsOf(user.uuid());
var stmt = conn.prepareStatement(DROP_USER);
stmt.setString(1, user.uuid());
stmt.execute();
conn.commit();
} catch (SQLException e) {
throw new RuntimeException(e);
}
return this;
}
private void dropPermissionsOf(String uuid) throws SQLException {
var stmt = conn.prepareStatement(DROP_PERMISSIONS);
stmt.setString(1, uuid);
stmt.execute();
}
@Override
public Set<User> find(String idOrEmail) {
try {
var result = new HashSet<User>();
var stmt = conn.prepareStatement(FIND_USER); // TODO: implement test for this query
stmt.setString(1, idOrEmail);
stmt.setString(2, "%" + idOrEmail + "%");
stmt.setString(3, "%" + idOrEmail + "%");
stmt.setString(4, idOrEmail);
stmt.setString(5, idOrEmail);
var rs = stmt.executeQuery();
while (rs.next()) result.add(userFrom(rs));
rs.close();
return result;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public Optional<User> forToken(String id) {
AccessToken token = accessTokens.get(id);
if (token == null) return empty();
if (token.valid()) return Optional.of(token.user());
accessTokens.remove(id);
return empty();
}
@Override
public UserService init(User defaultUser) {
try {
var rs = conn.prepareStatement(COUNT_USERS).executeQuery();
var count = rs.next() ? rs.getInt(1) : 0;
rs.close();
if (count < 1) save(defaultUser);
} catch (SQLException e) {
throw new RuntimeException(e);
}
return this;
}
@Override
protected void initTables() throws SQLException {
var rs = conn.prepareStatement(SELECT_STORE_VERSION).executeQuery();
int availableVersion = 1;
int currentVersion;
if (rs.next()) {
currentVersion = rs.getInt("value");
rs.close();
} else {
rs.close();
conn.prepareStatement(CREATE_STORE_VERSION).execute();
currentVersion = 0;
}
conn.setAutoCommit(false);
var stmt = conn.prepareStatement(SET_STORE_VERSION);
while (currentVersion < availableVersion) {
try {
switch (currentVersion) {
case 0:
createStoreTables();
break;
}
stmt.setInt(1, ++currentVersion);
stmt.execute();
conn.commit();
} catch (Exception e) {
conn.rollback();
LOG.log(System.Logger.Level.ERROR, "Failed to update at {} = {}", STORE_VERSION, currentVersion);
break;
}
}
conn.setAutoCommit(true);
}
@Override
public List<User> list() {
try {
List<User> result = new ArrayList<>();
var rs = conn.prepareStatement(LIST_USERS).executeQuery();
while (rs.next()) result.add(userFrom(rs));
rs.close();
for (User user : result) addPermissions(user);
return result;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public Optional<User> load(String id) {
try {
User user = null;
var stmt = conn.prepareStatement(LOAD_USER);
stmt.setString(1, id);
var rs = stmt.executeQuery();
if (rs.next()) user = userFrom(rs);
rs.close();
return nullable(user).map(this::addPermissions);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public Optional<User> load(String username, String password) {
var candidates = find(username);
for (var user : candidates) {
if (passwordMatches(password, user)) return Optional.of(user);
}
return empty();
}
@Override
public boolean passwordMatches(String password, User user) {
return hasher.matches(password, user.hashedPassword());
}
@Override
public SqliteUserService save(User user) {
try {
conn.setAutoCommit(false);
var stmt = conn.prepareStatement(SAVE_USER);
stmt.setString(1, user.uuid());
stmt.setString(2, user.hashedPassword());
stmt.setString(3, user.email());
stmt.setLong(4, user.sessionDuration().toMinutes());
stmt.setString(5, user.username());
stmt.setString(6, user.realName());
stmt.setString(7, user.hashedPassword());
stmt.setString(8, user.email());
stmt.setLong(9, user.sessionDuration().toMinutes());
stmt.setString(10, user.username());
stmt.setString(11, user.realName());
stmt.execute();
dropPermissionsOf(user.uuid());
stmt = conn.prepareStatement(INSERT_PERMISSIONS);
stmt.setString(1, user.uuid());
for (Permission perm : Permission.values()) {
if (user.hasPermission(perm)) {
stmt.setString(2, perm.toString());
stmt.execute();
}
}
conn.commit();
return this;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public SqliteUserService updatePassword(User user, String plaintextPassword) {
return save(user.hashedPassword(hasher.hash(plaintextPassword, uuid())));
}
private User userFrom(ResultSet rs) throws SQLException {
var uuid = rs.getString("uuid");
var pass = rs.getString("password");
var user = rs.getString("username");
var name = rs.getString("realname");
var mail = rs.getString("email");
var mins = rs.getLong("session_duration");
return new User(user, pass, name, mail, uuid).sessionDuration(Duration.ofMinutes(mins));
}
}

26
de.srsoftware.oidc.datastore.sqlite/src/test/java/de/srsoftware/oidc/datastore/sqlite/SqliteAuthServiceTest.java

@ -1,26 +0,0 @@
/* © SRSoftware 2024 */
package de.srsoftware.oidc.datastore.sqlite;
import static de.srsoftware.utils.Strings.uuid;
import de.srsoftware.oidc.api.AuthServiceTest;
import de.srsoftware.oidc.api.AuthorizationService;
import java.io.File;
import java.sql.SQLException;
import org.junit.jupiter.api.BeforeEach;
public class SqliteAuthServiceTest extends AuthServiceTest {
private AuthorizationService authorizationService;
@Override
protected AuthorizationService authorizationService() {
return authorizationService;
}
@BeforeEach
public void setup() throws SQLException {
var dbFile = new File("/tmp/" + uuid() + ".sqlite");
var conn = new ConnectionProvider().get(dbFile);
authorizationService = new SqliteAuthService(conn);
}
}

26
de.srsoftware.oidc.datastore.sqlite/src/test/java/de/srsoftware/oidc/datastore/sqlite/SqliteClientServiceTest.java

@ -1,26 +0,0 @@
/* © SRSoftware 2024 */
package de.srsoftware.oidc.datastore.sqlite;
import static de.srsoftware.utils.Strings.uuid;
import de.srsoftware.oidc.api.ClientService;
import de.srsoftware.oidc.api.ClientServiceTest;
import java.io.File;
import java.sql.SQLException;
import org.junit.jupiter.api.BeforeEach;
public class SqliteClientServiceTest extends ClientServiceTest {
private ClientService clientService;
@Override
protected ClientService clientService() {
return clientService;
}
@BeforeEach
public void setup() throws SQLException {
var dbFile = new File("/tmp/" + uuid() + ".sqlite");
var conn = new ConnectionProvider().get(dbFile);
clientService = new SqliteClientService(conn);
}
}

26
de.srsoftware.oidc.datastore.sqlite/src/test/java/de/srsoftware/oidc/datastore/sqlite/SqliteKeyStoreTest.java

@ -1,26 +0,0 @@
/* © SRSoftware 2024 */
package de.srsoftware.oidc.datastore.sqlite;
import static de.srsoftware.utils.Strings.uuid;
import de.srsoftware.oidc.api.KeyStorage;
import de.srsoftware.oidc.api.KeyStoreTest;
import java.io.File;
import java.sql.SQLException;
import org.junit.jupiter.api.BeforeEach;
public class SqliteKeyStoreTest extends KeyStoreTest {
private KeyStorage keyStore;
@Override
protected KeyStorage keyStore() {
return keyStore;
}
@BeforeEach
public void setup() throws SQLException {
var dbFile = new File("/tmp/" + uuid() + ".sqlite");
var conn = new ConnectionProvider().get(dbFile);
keyStore = new SqliteKeyStore(conn);
}
}

37
de.srsoftware.oidc.datastore.sqlite/src/test/java/de/srsoftware/oidc/datastore/sqlite/SqliteMailConfigTest.java

@ -1,37 +0,0 @@
/* © SRSoftware 2024 */
package de.srsoftware.oidc.datastore.sqlite;
import static de.srsoftware.utils.Strings.uuid;
import de.srsoftware.oidc.api.MailConfig;
import de.srsoftware.oidc.api.MailConfigTest;
import java.io.File;
import java.sql.SQLException;
import org.junit.jupiter.api.BeforeEach;
public class SqliteMailConfigTest extends MailConfigTest {
private SqliteMailConfig mailConfig;
private File dbFile;
@Override
protected MailConfig mailConfig() {
return mailConfig;
}
@Override
protected void reOpen() {
try {
var conn = new ConnectionProvider().get(dbFile);
mailConfig = new SqliteMailConfig(conn);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@BeforeEach
public void setup() throws SQLException {
dbFile = new File("/tmp/" + uuid() + ".sqlite");
var conn = new ConnectionProvider().get(dbFile);
mailConfig = new SqliteMailConfig(conn);
}
}

32
de.srsoftware.oidc.datastore.sqlite/src/test/java/de/srsoftware/oidc/datastore/sqlite/SqliteSessionServiceTest.java

@ -1,32 +0,0 @@
/* © SRSoftware 2024 */
package de.srsoftware.oidc.datastore.sqlite;
import de.srsoftware.oidc.api.SessionService;
import de.srsoftware.oidc.api.SessionServiceTest;
import java.io.File;
import java.sql.SQLException;
import java.util.UUID;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
public class SqliteSessionServiceTest extends SessionServiceTest {
private File storage = new File("/tmp/" + UUID.randomUUID());
private SessionService sessionService = null;
@AfterEach
public void tearDown() {
if (storage.exists()) storage.delete();
}
@BeforeEach
public void setup() throws SQLException {
tearDown();
sessionService = new SqliteSessionService(new ConnectionProvider().get(storage));
}
@Override
protected SessionService sessionService() {
return sessionService;
}
}

32
de.srsoftware.oidc.datastore.sqlite/src/test/java/de/srsoftware/oidc/datastore/sqlite/SqliteUserServiceTest.java

@ -1,32 +0,0 @@
/* © SRSoftware 2024 */
package de.srsoftware.oidc.datastore.sqlite;
import de.srsoftware.oidc.api.UserService;
import de.srsoftware.oidc.api.UserServiceTest;
import java.io.File;
import java.sql.SQLException;
import java.util.UUID;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
public class SqliteUserServiceTest extends UserServiceTest {
private File storage = new File("/tmp/" + UUID.randomUUID());
private UserService userService;
@AfterEach
public void tearDown() {
if (storage.exists()) storage.delete();
}
@BeforeEach
public void setup() throws SQLException {
tearDown();
userService = new SqliteUserService(new ConnectionProvider().get(storage), hasher());
}
@Override
protected UserService userService() {
return userService;
}
}

3
settings.gradle

@ -4,9 +4,8 @@ include 'de.srsoftware.logging'
include 'de.srsoftware.oidc.api' include 'de.srsoftware.oidc.api'
include 'de.srsoftware.oidc.app' include 'de.srsoftware.oidc.app'
include 'de.srsoftware.oidc.backend' include 'de.srsoftware.oidc.backend'
include 'de.srsoftware.oidc.datastore.encrypted'
include 'de.srsoftware.oidc.datastore.file' include 'de.srsoftware.oidc.datastore.file'
include 'de.srsoftware.oidc.web' include 'de.srsoftware.oidc.web'
include 'de.srsoftware.utils' include 'de.srsoftware.utils'
include 'de.srsoftware.oidc.datastore.sqlite'
include 'de.srsoftware.oidc.datastore.encrypted'

Loading…
Cancel
Save