From cd3a5b39e3259caba0ab3af9d8b26f27fe1cf727 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Sun, 15 Sep 2024 11:53:38 +0200 Subject: [PATCH] implemented SqliteAuthService Signed-off-by: Stephan Richter --- .../srsoftware/oidc/api/AuthServiceTest.java | 4 +- .../de/srsoftware/oidc/app/Application.java | 2 +- .../datastore/sqlite/SqliteAuthService.java | 122 +++++++++++++++++- .../sqlite/SqliteAuthServiceTest.java | 26 ++++ 4 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 de.srsoftware.oidc.datastore.sqlite/src/test/java/de/srsoftware/oidc/datastore/sqlite/SqliteAuthServiceTest.java diff --git a/de.srsoftware.oidc.api/src/test/java/de/srsoftware/oidc/api/AuthServiceTest.java b/de.srsoftware.oidc.api/src/test/java/de/srsoftware/oidc/api/AuthServiceTest.java index 720fa3c..9be301a 100644 --- a/de.srsoftware.oidc.api/src/test/java/de/srsoftware/oidc/api/AuthServiceTest.java +++ b/de.srsoftware.oidc.api/src/test/java/de/srsoftware/oidc/api/AuthServiceTest.java @@ -25,8 +25,10 @@ public abstract class AuthServiceTest { public void testAuthorize() { var authorizationService = authorizationService(); var userId1 = uuid(); - var expiration = Instant.now().plusSeconds(3600).truncatedTo(SECONDS); + var expiration = Instant.now(); authorizationService.authorize(userId1, CLIENT1, SCOPES1, expiration); + expiration = Instant.now().plusSeconds(3600).truncatedTo(SECONDS); // test overwrite + authorizationService.authorize(userId1, CLIENT1, SCOPES1, expiration); // test overwrite var authorization = authorizationService.getAuthorization(userId1, CLIENT1, Set.of(OPENID)); assertEquals(1, authorization.authorizedScopes().scopes().size()); assertTrue(authorization.authorizedScopes().scopes().contains(OPENID)); 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 485a3b8..8bfd9e9 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 @@ -92,7 +92,7 @@ public class Application { }; } - private static AuthorizationService setupAuthService(Configuration config, Path defaultFile, FileStoreProvider fileStoreProvider) { + private static AuthorizationService setupAuthService(Configuration config, Path defaultFile, FileStoreProvider fileStoreProvider) throws SQLException { var authServiceLocation = new File(config.getOrDefault("auth_store",defaultFile)); return switch (extension(authServiceLocation)){ case "db", "sqlite", "sqlite3" -> new SqliteAuthService(connectionProvider.get(authServiceLocation)); 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 index 17d233d..9180417 100644 --- 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 @@ -1,30 +1,138 @@ /* © 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.util.Collection; -import java.util.Optional; +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 Map authCodes = 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); + } -public class SqliteAuthService implements AuthorizationService { - public SqliteAuthService(Connection connection) { + private String authCode(Authorization authorization) { + var code = uuid(); + authCodes.put(code, authorization); + return code; } @Override public AuthorizationService authorize(String userId, String clientId, Collection scopes, Instant expiration) { - return null; + 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 Optional consumeAuthorization(String authCode) { - return Optional.empty(); + return nullable(authCodes.remove(authCode)); } @Override public AuthResult getAuthorization(String userId, String clientId, Collection scopes) { - return null; + 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(scopes); + var authorized = new HashSet(); + 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); + } } } diff --git a/de.srsoftware.oidc.datastore.sqlite/src/test/java/de/srsoftware/oidc/datastore/sqlite/SqliteAuthServiceTest.java b/de.srsoftware.oidc.datastore.sqlite/src/test/java/de/srsoftware/oidc/datastore/sqlite/SqliteAuthServiceTest.java new file mode 100644 index 0000000..b5a5561 --- /dev/null +++ b/de.srsoftware.oidc.datastore.sqlite/src/test/java/de/srsoftware/oidc/datastore/sqlite/SqliteAuthServiceTest.java @@ -0,0 +1,26 @@ +/* © 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); + } +}