Browse Source

decoupling sesson object from user object

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
sqlite
Stephan Richter 2 months ago
parent
commit
2f4726d1e7
  1. 5
      de.srsoftware.http/src/main/java/de/srsoftware/http/PathHandler.java
  2. 4
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionService.java
  3. 2
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Session.java
  4. 37
      de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/ClientController.java
  5. 12
      de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/Controller.java
  6. 25
      de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/EmailController.java
  7. 33
      de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java
  8. 21
      de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java
  9. 26
      de.srsoftware.oidc.datastore.file/src/test/java/de/srsoftware/oidc/datastore/file/SessionServiceTest.java
  10. 5
      de.srsoftware.oidc.datastore.sqlite/src/main/java/de/srsoftware/oidc/datastore/sqlite/SqliteSessionService.java

5
de.srsoftware.http/src/main/java/de/srsoftware/http/PathHandler.java

@ -187,6 +187,11 @@ public abstract class PathHandler implements HttpHandler { @@ -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();
}

4
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionService.java

@ -9,7 +9,7 @@ import java.util.Optional; @@ -9,7 +9,7 @@ import java.util.Optional;
public interface SessionService {
Session createSession(User user);
SessionService dropSession(String sessionId);
Session extend(Session session);
Optional<Session> retrieve(String sessionId, UserService userService);
Session extend(Session session, User user);
Optional<Session> retrieve(String sessionId);
SessionService setDuration(Duration duration);
}

2
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Session.java

@ -3,5 +3,5 @@ package de.srsoftware.oidc.api.data; @@ -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) {
}

37
de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/ClientController.java

@ -22,11 +22,13 @@ public class ClientController extends Controller { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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<String>();
for (Object o : json.getJSONArray(REDIRECT_URIS)) {

12
de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/Controller.java

@ -5,20 +5,22 @@ import com.sun.net.httpserver.HttpExchange; @@ -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<Session> 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");
}
}

25
de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/EmailController.java

@ -14,11 +14,13 @@ import de.srsoftware.oidc.api.data.Session; @@ -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 { @@ -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 { @@ -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 { @@ -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));

33
de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java

@ -30,7 +30,7 @@ public class UserController extends Controller { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -192,7 +195,7 @@ public class UserController extends Controller {
var password = body.has(PASSWORD) ? body.getString(PASSWORD) : null;
Optional<User> 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 { @@ -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));
}

21
de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java

@ -21,7 +21,6 @@ import java.time.Duration; @@ -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 @@ -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 @@ -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 @@ -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 @@ -250,14 +243,12 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
}
@Override
public Optional<Session> retrieve(String sessionId, UserService userService) {
public Optional<Session> 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 @@ -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;
}

26
de.srsoftware.oidc.datastore.file/src/test/java/de/srsoftware/oidc/datastore/file/SessionServiceTest.java

@ -1,20 +1,33 @@ @@ -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<String> 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<String> hasher() {
if (hasher == null) try {
hasher = new UuidHasher();
@ -30,4 +43,17 @@ public class SessionServiceTest { @@ -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)));
}
}

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

@ -2,7 +2,6 @@ @@ -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 { @@ -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<Session> retrieve(String sessionId, UserService users) {
public Optional<Session> retrieve(String sessionId) {
return Optional.empty();
}

Loading…
Cancel
Save