diff --git a/de.srsoftware.http/src/main/java/de/srsoftware/http/PathHandler.java b/de.srsoftware.http/src/main/java/de/srsoftware/http/PathHandler.java index dc7e381..7b95947 100644 --- a/de.srsoftware.http/src/main/java/de/srsoftware/http/PathHandler.java +++ b/de.srsoftware.http/src/main/java/de/srsoftware/http/PathHandler.java @@ -187,6 +187,11 @@ public abstract class PathHandler implements HttpHandler { return sendContent(ex, HTTP_OK, o); } + public static boolean serverError(HttpExchange ex, Object o) throws IOException { + sendContent(ex, HTTP_INTERNAL_ERROR, o); + return false; + } + public static String url(HttpExchange ex) { return hostname(ex) + ex.getRequestURI(); } diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionService.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionService.java index 5159565..65c7034 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionService.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionService.java @@ -9,7 +9,7 @@ import java.util.Optional; public interface SessionService { Session createSession(User user); SessionService dropSession(String sessionId); - Session extend(Session session); - Optional retrieve(String sessionId, UserService userService); + Session extend(Session session, User user); + Optional retrieve(String sessionId); SessionService setDuration(Duration duration); } diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Session.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Session.java index f39d6ea..723d571 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Session.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Session.java @@ -3,5 +3,5 @@ package de.srsoftware.oidc.api.data; import java.time.Instant; -public record Session(User user, Instant expiration, String id) { +public record Session(String userId, Instant expiration, String id) { } diff --git a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/ClientController.java b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/ClientController.java index 2d07b81..ed8046b 100644 --- a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/ClientController.java +++ b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/ClientController.java @@ -22,11 +22,13 @@ public class ClientController extends Controller { private static final System.Logger LOG = System.getLogger(ClientController.class.getSimpleName()); private final AuthorizationService authorizations; private final ClientService clients; + private final UserService users; public ClientController(AuthorizationService authorizationService, ClientService clientService, SessionService sessionService, UserService userService) { - super(sessionService, userService); + super(sessionService); authorizations = authorizationService; clients = clientService; + users = userService; } private boolean authorizationError(HttpExchange ex, String errorCode, String description, String state) throws IOException { @@ -38,7 +40,9 @@ public class ClientController extends Controller { } private boolean authorize(HttpExchange ex, Session session) throws IOException { - var user = session.user(); + var optUser = users.load(session.userId()); + if (optUser.isEmpty()) return invalidSessionUser(ex); + var user = optUser.get(); var json = json(ex); var state = json.has(STATE) ? json.getString(STATE) : null; if (!json.has(CLIENT_ID)) return authorizationError(ex, INVALID_REQUEST, "Missing required parameter \"%s\"!".formatted(CLIENT_ID), state); @@ -95,7 +99,9 @@ public class ClientController extends Controller { } private boolean deleteClient(HttpExchange ex, Session session) throws IOException { - if (!session.user().hasPermission(MANAGE_CLIENTS)) return badRequest(ex, "NOT ALLOWED"); + var optUser = users.load(session.userId()); + if (optUser.isEmpty()) return invalidSessionUser(ex); + if (!optUser.get().hasPermission(MANAGE_CLIENTS)) return badRequest(ex, "NOT ALLOWED"); var json = json(ex); var id = json.getString(CLIENT_ID); clients.getClient(id).ifPresent(clients::remove); @@ -110,7 +116,11 @@ public class ClientController extends Controller { // post-login paths var session = optSession.get(); - sessions.extend(session); + var optUser = users.load(session.userId()); + if (optUser.isEmpty()) return invalidSessionUser(ex); + var user = optUser.get(); + sessions.extend(session, user); + switch (path) { case "/": return deleteClient(ex, session); @@ -126,7 +136,11 @@ public class ClientController extends Controller { // post-login paths var session = optSession.get(); - sessions.extend(session); + var optUser = users.load(session.userId()); + if (optUser.isEmpty()) return invalidSessionUser(ex); + var user = optUser.get(); + sessions.extend(session, user); + switch (path) { case "/": return load(ex, session); @@ -141,8 +155,9 @@ public class ClientController extends Controller { } private boolean list(HttpExchange ex, Session session) throws IOException { - var user = session.user(); - if (!user.hasPermission(MANAGE_CLIENTS)) return sendEmptyResponse(HTTP_FORBIDDEN, ex); + var optUser = users.load(session.userId()); + if (optUser.isEmpty()) return invalidSessionUser(ex); + if (!optUser.get().hasPermission(MANAGE_CLIENTS)) return sendEmptyResponse(HTTP_FORBIDDEN, ex); var json = new JSONObject(); clients.listClients().forEach(client -> json.put(client.id(), Map.of("name", client.name(), "redirect_uris", client.redirectUris()))); return sendContent(ex, json); @@ -150,7 +165,9 @@ public class ClientController extends Controller { private boolean load(HttpExchange ex, Session session) throws IOException { - if (!session.user().hasPermission(MANAGE_CLIENTS)) return sendEmptyResponse(HTTP_FORBIDDEN, ex); + var optUser = users.load(session.userId()); + if (optUser.isEmpty()) return invalidSessionUser(ex); + if (!optUser.get().hasPermission(MANAGE_CLIENTS)) return sendEmptyResponse(HTTP_FORBIDDEN, ex); var json = json(ex); if (json.has(CLIENT_ID)) { var clientID = json.getString(CLIENT_ID); @@ -161,7 +178,9 @@ public class ClientController extends Controller { } private boolean save(HttpExchange ex, Session session) throws IOException { - if (!session.user().hasPermission(MANAGE_CLIENTS)) return badRequest(ex, "NOT ALLOWED"); + var optUser = users.load(session.userId()); + if (optUser.isEmpty()) return invalidSessionUser(ex); + if (!optUser.get().hasPermission(MANAGE_CLIENTS)) return badRequest(ex, "NOT ALLOWED"); var json = json(ex); var redirects = new HashSet(); for (Object o : json.getJSONArray(REDIRECT_URIS)) { diff --git a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/Controller.java b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/Controller.java index 8ce2fd9..604c8d3 100644 --- a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/Controller.java +++ b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/Controller.java @@ -5,20 +5,22 @@ import com.sun.net.httpserver.HttpExchange; import de.srsoftware.http.PathHandler; import de.srsoftware.http.SessionToken; import de.srsoftware.oidc.api.SessionService; -import de.srsoftware.oidc.api.UserService; import de.srsoftware.oidc.api.data.Session; +import java.io.IOException; import java.util.Optional; public abstract class Controller extends PathHandler { protected final SessionService sessions; - private final UserService users; - Controller(SessionService sessionService, UserService userService) { + Controller(SessionService sessionService) { sessions = sessionService; - users = userService; } protected Optional getSession(HttpExchange ex) { - return SessionToken.from(ex).map(SessionToken::sessionId).flatMap(sessionId -> sessions.retrieve(sessionId, users)); + return SessionToken.from(ex).map(SessionToken::sessionId).flatMap(sessionId -> sessions.retrieve(sessionId)); + } + + protected boolean invalidSessionUser(HttpExchange ex) throws IOException { + return serverError(ex, "Session object refers to missing user"); } } diff --git a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/EmailController.java b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/EmailController.java index 6b4bf08..7a1ccdc 100644 --- a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/EmailController.java +++ b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/EmailController.java @@ -14,11 +14,13 @@ import de.srsoftware.oidc.api.data.Session; import java.io.IOException; public class EmailController extends Controller { - private final MailConfig mailConfig; + private final MailConfig mailConfig; + private final UserService users; public EmailController(MailConfig mailConfig, SessionService sessionService, UserService userService) { - super(sessionService, userService); + super(sessionService); this.mailConfig = mailConfig; + users = userService; } @Override @@ -26,7 +28,11 @@ public class EmailController extends Controller { var optSession = getSession(ex); if (optSession.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); var session = optSession.get(); - sessions.extend(session); + var optUser = users.load(session.userId()); + if (optUser.isEmpty()) return invalidSessionUser(ex); + var user = optUser.get(); + sessions.extend(session, user); + switch (path) { case "/settings": return provideSettings(ex, session); @@ -39,7 +45,10 @@ public class EmailController extends Controller { var optSession = getSession(ex); if (optSession.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); var session = optSession.get(); - sessions.extend(session); + var optUser = users.load(session.userId()); + if (optUser.isEmpty()) return invalidSessionUser(ex); + var user = optUser.get(); + sessions.extend(session, user); switch (path) { case "/settings": @@ -49,12 +58,16 @@ public class EmailController extends Controller { } private boolean provideSettings(HttpExchange ex, Session session) throws IOException { - if (!session.user().hasPermission(MANAGE_SMTP)) return sendEmptyResponse(HTTP_FORBIDDEN, ex); + var optUser = users.load(session.userId()); + if (optUser.isEmpty()) return invalidSessionUser(ex); + if (!optUser.get().hasPermission(MANAGE_SMTP)) return sendEmptyResponse(HTTP_FORBIDDEN, ex); return sendContent(ex, mailConfig.map()); } private boolean saveSettings(HttpExchange ex, Session session) throws IOException { - if (!session.user().hasPermission(MANAGE_SMTP)) return sendEmptyResponse(HTTP_FORBIDDEN, ex); + var optUser = users.load(session.userId()); + if (optUser.isEmpty()) return invalidSessionUser(ex); + if (!optUser.get().hasPermission(MANAGE_SMTP)) return sendEmptyResponse(HTTP_FORBIDDEN, ex); var data = json(ex); if (data.has(SMTP_HOST)) mailConfig.smtpHost(data.getString(SMTP_HOST)); if (data.has(SMTP_PORT)) mailConfig.smtpPort(data.getInt(SMTP_PORT)); diff --git a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java index 9a3ef1c..a87cac7 100644 --- a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java +++ b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java @@ -30,7 +30,7 @@ public class UserController extends Controller { private final ResourceLoader resourceLoader; public UserController(MailConfig mailConfig, SessionService sessionService, UserService userService, ResourceLoader resourceLoader) { - super(sessionService, userService); + super(sessionService); users = userService; this.mailConfig = mailConfig; this.resourceLoader = resourceLoader; @@ -51,9 +51,11 @@ public class UserController extends Controller { public boolean doDelete(String path, HttpExchange ex) throws IOException { var optSession = getSession(ex); if (optSession.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); - - // post-login paths - var user = sessions.extend(optSession.get()).user(); + var session = optSession.get(); + var optUser = users.load(session.userId()); + if (optUser.isEmpty()) return invalidSessionUser(ex); + var user = optUser.get(); + sessions.extend(session, user); switch (path) { case "/delete": @@ -88,10 +90,11 @@ public class UserController extends Controller { } var optSession = getSession(ex); if (optSession.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); - - // post-login paths var session = optSession.get(); - sessions.extend(session); + var optUser = users.load(session.userId()); + if (optUser.isEmpty()) return invalidSessionUser(ex); + var user = optUser.get(); + sessions.extend(session, user); switch (path) { case "/logout": @@ -112,15 +115,15 @@ public class UserController extends Controller { } var optSession = getSession(ex); if (optSession.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); - - // post-login paths var session = optSession.get(); - sessions.extend(session); - var user = session.user(); + var optUser = users.load(session.userId()); + if (optUser.isEmpty()) return invalidSessionUser(ex); + var user = optUser.get(); + sessions.extend(session, user); switch (path) { case "/": - return sendUserAndCookie(ex, session); + return sendUserAndCookie(ex, session, user); case "/add": return addUser(ex, user); case "/list": @@ -192,7 +195,7 @@ public class UserController extends Controller { var password = body.has(PASSWORD) ? body.getString(PASSWORD) : null; Optional user = users.load(username, password); - if (user.isPresent()) return sendUserAndCookie(ex, sessions.createSession(user.get())); + if (user.isPresent()) return sendUserAndCookie(ex, sessions.createSession(user.get()), user.get()); return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); } @@ -262,9 +265,9 @@ public class UserController extends Controller { } } - private boolean sendUserAndCookie(HttpExchange ex, Session session) throws IOException { + private boolean sendUserAndCookie(HttpExchange ex, Session session, User user) throws IOException { new SessionToken(session.id()).addTo(ex); - return sendContent(ex, session.user().map(false)); + return sendContent(ex, user.map(false)); } diff --git a/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java b/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java index cac543f..ecb513b 100644 --- a/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java +++ b/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java @@ -21,7 +21,6 @@ import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.*; -import java.util.stream.Collectors; import org.json.JSONObject; public class FileStore implements AuthorizationService, ClientService, SessionService, UserService, MailConfig { @@ -75,12 +74,7 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe var authorizations = json.getJSONObject(AUTHORIZATIONS); var authorizationUsers = Set.copyOf(authorizations.keySet()); - var userIds = list().stream().map(User::uuid).collect(Collectors.toSet()); for (var userId : authorizationUsers) { - if (!userIds.contains(userId)) { - authorizations.remove(userId); - continue; - } var clients = authorizations.getJSONObject(userId); var clientIds = Set.copyOf(clients.keySet()); for (var clientId : clientIds) { @@ -228,7 +222,7 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe public Session createSession(User user) { var now = Instant.now(); var endOfSession = now.plus(user.sessionDuration()); - return save(new Session(user, endOfSession, uuid())); + return save(new Session(user.uuid(), endOfSession, uuid())); } @Override @@ -239,10 +233,9 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe } @Override - public Session extend(Session session) { - var user = session.user(); + public Session extend(Session session, User user) { var endOfSession = Instant.now().plus(user.sessionDuration()); - return save(new Session(user, endOfSession, session.id())); + return save(new Session(user.uuid(), endOfSession, session.id())); } private JSONObject sessions() { @@ -250,14 +243,12 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe } @Override - public Optional retrieve(String sessionId, UserService userService) { + public Optional retrieve(String sessionId) { try { var session = sessions().getJSONObject(sessionId); var userId = session.getString(USER); var expiration = Instant.ofEpochSecond(session.getLong(EXPIRATION)); - if (expiration.isAfter(Instant.now())) { - return userService.load(userId).map(user -> new Session(user, expiration, sessionId)); - } + if (expiration.isAfter(Instant.now())) return Optional.of(new Session(userId, expiration, sessionId)); dropSession(sessionId); } catch (Exception ignored) { } @@ -265,7 +256,7 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe } private Session save(Session session) { - sessions().put(session.id(), Map.of(USER, session.user().uuid(), EXPIRATION, session.expiration().getEpochSecond())); + sessions().put(session.id(), Map.of(USER, session.userId(), EXPIRATION, session.expiration().getEpochSecond())); save(); return session; } diff --git a/de.srsoftware.oidc.datastore.file/src/test/java/de/srsoftware/oidc/datastore/file/SessionServiceTest.java b/de.srsoftware.oidc.datastore.file/src/test/java/de/srsoftware/oidc/datastore/file/SessionServiceTest.java index dffdc7e..9ec6fd6 100644 --- a/de.srsoftware.oidc.datastore.file/src/test/java/de/srsoftware/oidc/datastore/file/SessionServiceTest.java +++ b/de.srsoftware.oidc.datastore.file/src/test/java/de/srsoftware/oidc/datastore/file/SessionServiceTest.java @@ -1,20 +1,33 @@ /* © SRSoftware 2024 */ package de.srsoftware.oidc.datastore.file; +import static de.srsoftware.utils.Strings.uuid; +import static org.junit.jupiter.api.Assertions.assertTrue; + import de.srsoftware.oidc.api.SessionService; +import de.srsoftware.oidc.api.data.User; import de.srsoftware.utils.PasswordHasher; import de.srsoftware.utils.UuidHasher; import java.io.File; import java.io.IOException; import java.security.NoSuchAlgorithmException; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.UUID; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; public class SessionServiceTest { private PasswordHasher hasher = null; private File storage = new File("/tmp/" + UUID.randomUUID()); private SessionService sessionService; + private static final String EMAIL = "arno@nym.de"; + private static final String PASSWORD = "grunzwanzling"; + private static final String REALNAME = "Arno Nym"; + private static final String USERNAME = "arno"; + protected PasswordHasher hasher() { if (hasher == null) try { hasher = new UuidHasher(); @@ -30,4 +43,17 @@ public class SessionServiceTest { if (storage.exists()) storage.delete(); sessionService = new FileStore(storage, hasher()); } + + @Test + public void testCreate() { + var uuid = uuid(); + var pass = hasher().hash(PASSWORD, uuid); + var user = new User(USERNAME, pass, REALNAME, EMAIL, uuid).sessionDuration(Duration.ofMinutes(5)); + + Instant now = Instant.now(); + var session = sessionService.createSession(user); + var expiration = session.expiration(); + assertTrue(expiration.isAfter(now.plus(5, ChronoUnit.MINUTES).minusSeconds(1))); + assertTrue(expiration.isBefore(now.plus(5, ChronoUnit.MINUTES).plusSeconds(1))); + } } 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 index 5ab2d43..46bcf8d 100644 --- 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 @@ -2,7 +2,6 @@ package de.srsoftware.oidc.datastore.sqlite; import de.srsoftware.oidc.api.SessionService; -import de.srsoftware.oidc.api.UserService; import de.srsoftware.oidc.api.data.Session; import de.srsoftware.oidc.api.data.User; import java.sql.Connection; @@ -24,12 +23,12 @@ public class SqliteSessionService implements SessionService { } @Override - public Session extend(Session session) { + public Session extend(Session session, User user) { return null; } @Override - public Optional retrieve(String sessionId, UserService users) { + public Optional retrieve(String sessionId) { return Optional.empty(); }