From 59075db1ad2a6ecf22216ee97ff161a6c4638ba5 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Fri, 19 Jul 2024 00:09:38 +0200 Subject: [PATCH] started to implement sessions Signed-off-by: Stephan Richter --- .../java/de/srsoftware/oidc/api/Cookie.java | 5 + .../java/de/srsoftware/oidc/api/Session.java | 7 ++ .../srsoftware/oidc/api/SessionService.java | 13 ++ .../de/srsoftware/oidc/api/SessionToken.java | 18 ++- .../de/srsoftware/oidc/api/UserService.java | 11 +- .../de/srsoftware/oidc/app/Application.java | 17 +-- .../de/srsoftware/oidc/backend/Backend.java | 31 ++--- .../oidc/datastore/file/FileStore.java | 117 ++++++++++++++---- 8 files changed, 167 insertions(+), 52 deletions(-) create mode 100644 de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Session.java create mode 100644 de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionService.java diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Cookie.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Cookie.java index 0a810ae..5552215 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Cookie.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Cookie.java @@ -3,6 +3,7 @@ package de.srsoftware.oidc.api; import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpExchange; +import java.util.List; import java.util.Map; public abstract class Cookie implements Map.Entry { @@ -33,6 +34,10 @@ public abstract class Cookie implements Map.Entry { return value; } + protected static List of(HttpExchange ex) { + return ex.getRequestHeaders().get("Cookie"); + } + @Override public String setValue(String s) { var oldVal = value; diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Session.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Session.java new file mode 100644 index 0000000..3c98cdc --- /dev/null +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Session.java @@ -0,0 +1,7 @@ +/* © SRSoftware 2024 */ +package de.srsoftware.oidc.api; + +import java.time.Instant; + +public record Session(User user, Instant expiration, String id) { +} 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 new file mode 100644 index 0000000..8aa563a --- /dev/null +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionService.java @@ -0,0 +1,13 @@ +/* © SRSoftware 2024 */ +package de.srsoftware.oidc.api; + +import java.time.Duration; +import java.util.Optional; + +public interface SessionService { + Session createSession(User user); + SessionService dropSession(String sessionId); + Session extend(String sessionId); + Optional retrieve(String sessionId); + SessionService setDuration(Duration duration); +} diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionToken.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionToken.java index bce0387..6ab8c23 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionToken.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionToken.java @@ -2,8 +2,22 @@ package de.srsoftware.oidc.api; +import com.sun.net.httpserver.HttpExchange; +import java.util.Optional; + public class SessionToken extends Cookie { - public SessionToken(String value) { - super("sessionToken", value); + private final String sessionId; + + public SessionToken(String sessionId) { + super("sessionToken", sessionId); + this.sessionId = sessionId; + } + + public static Optional from(HttpExchange ex) { + return Cookie.of(ex).stream().filter(cookie -> cookie.startsWith("sessionToken=")).map(cookie -> cookie.split("=", 2)[1]).map(id -> new SessionToken(id)).findAny(); + } + + public String sessionId() { + return sessionId; } } \ No newline at end of file diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/UserService.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/UserService.java index 674d1c3..ebe81aa 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/UserService.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/UserService.java @@ -5,9 +5,10 @@ import java.util.List; import java.util.Optional; public interface UserService { - public UserService delete(User user); - public UserService init(User defaultUser); - public List list(); - public Optional load(String username, String password); - public UserService save(User user); + public UserService delete(User user); + public UserService init(User defaultUser); + public List list(); + public Optional load(String id); + public Optional load(String username, String password); + public T save(User user); } 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 ef507c3..e8dde62 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 @@ -3,6 +3,7 @@ package de.srsoftware.oidc.app; import com.sun.net.httpserver.HttpServer; +import de.srsoftware.oidc.api.SessionService; import de.srsoftware.oidc.api.User; import de.srsoftware.oidc.api.UserService; import de.srsoftware.oidc.backend.Backend; @@ -23,15 +24,17 @@ public class Application { public static final String INDEX = STATIC_PATH + "/index.html"; public static void main(String[] args) throws Exception { - var storageFile = new File("/tmp/lightoidc.json"); - var passwordHasher = new UuidHasher(); - var firstHash = passwordHasher.hash(FIRST_USER_PASS, FIRST_UUID); - var firstUser = new User(FIRST_USER, firstHash, FIRST_USER, "%s@internal".formatted(FIRST_USER), FIRST_UUID); - UserService userService = new FileStore(storageFile, passwordHasher).init(firstUser); - HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); + var storageFile = new File("/tmp/lightoidc.json"); + var passwordHasher = new UuidHasher(); + var firstHash = passwordHasher.hash(FIRST_USER_PASS, FIRST_UUID); + var firstUser = new User(FIRST_USER, firstHash, FIRST_USER, "%s@internal".formatted(FIRST_USER), FIRST_UUID); + FileStore fileStore = new FileStore(storageFile, passwordHasher).init(firstUser); + UserService userService = fileStore; + SessionService sessionService = fileStore; + HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); new StaticPages().bindPath(STATIC_PATH).on(server); new Forward(INDEX).bindPath("/").on(server); - new Backend(userService).bindPath("/api").on(server); + new Backend(sessionService, userService).bindPath("/api").on(server); server.setExecutor(Executors.newCachedThreadPool()); server.start(); } diff --git a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/Backend.java b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/Backend.java index 9763f98..f46f1ef 100644 --- a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/Backend.java +++ b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/Backend.java @@ -6,22 +6,20 @@ import static de.srsoftware.oidc.api.User.USERNAME; import static java.net.HttpURLConnection.HTTP_NOT_FOUND; import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.jar.Attributes.Name.CONTENT_TYPE; import com.sun.net.httpserver.HttpExchange; -import de.srsoftware.oidc.api.PathHandler; -import de.srsoftware.oidc.api.SessionToken; -import de.srsoftware.oidc.api.User; -import de.srsoftware.oidc.api.UserService; +import de.srsoftware.oidc.api.*; import java.io.IOException; import java.util.Optional; import org.json.JSONObject; public class Backend extends PathHandler { - private final UserService users; + private final SessionService sessions; + private final UserService users; - public Backend(UserService userService) { - users = userService; + public Backend(SessionService sessionService, UserService userService) { + sessions = sessionService; + users = userService; } private void doLogin(HttpExchange ex) throws IOException { @@ -32,7 +30,8 @@ public class Backend extends PathHandler { Optional user = users.load(username, password); if (user.isPresent()) { - sendUserAndCookie(ex, user.get()); + var session = sessions.createSession(user.get()); + sendUserAndCookie(ex, session); return; } sendEmptyResponse(HTTP_UNAUTHORIZED, ex); @@ -44,12 +43,12 @@ public class Backend extends PathHandler { String method = ex.getRequestMethod(); System.out.printf("%s %s…", method, path); + var user = getSession(ex).map(Session::user); if ("login".equals(path) && POST.equals(method)) { doLogin(ex); // TODO: prevent brute force return; } - var token = getAuthToken(ex); - if (token.isEmpty()) { + if (user.isEmpty()) { sendEmptyResponse(HTTP_UNAUTHORIZED, ex); System.err.println("unauthorized"); return; @@ -59,12 +58,16 @@ public class Backend extends PathHandler { ex.getResponseBody().close(); } - private void sendUserAndCookie(HttpExchange ex, User user) throws IOException { - var bytes = new JSONObject(user.map(false)).toString().getBytes(UTF_8); + private Optional getSession(HttpExchange ex) { + return SessionToken.from(ex).map(SessionToken::sessionId).flatMap(sessions::retrieve); + } + + private void sendUserAndCookie(HttpExchange ex, Session session) throws IOException { + var bytes = new JSONObject(session.user().map(false)).toString().getBytes(UTF_8); var headers = ex.getResponseHeaders(); headers.add(CONTENT_TYPE, JSON); - new SessionToken("Test").addTo(headers); + new SessionToken(session.id()).addTo(headers); ex.sendResponseHeaders(200, bytes.length); ex.getResponseBody().write(bytes); } 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 e86f6f3..bba68b7 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 @@ -2,24 +2,28 @@ package de.srsoftware.oidc.datastore.file; /* © SRSoftware 2024 */ import static de.srsoftware.oidc.api.User.*; -import de.srsoftware.oidc.api.PasswordHasher; -import de.srsoftware.oidc.api.User; -import de.srsoftware.oidc.api.UserService; +import de.srsoftware.oidc.api.*; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.List; -import java.util.Optional; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.*; import org.json.JSONObject; -public class FileStore implements UserService { - private static final String USERS = "users"; +public class FileStore implements SessionService, UserService { + private static final String EXPIRATION = "expiration"; + private static final String SESSIONS = "sessions"; + private static final String USERS = "users"; + private static final String USER = "user"; private final Path storageFile; private final JSONObject json; private final PasswordHasher passwordHasher; + private Duration sessionDuration = Duration.of(10, ChronoUnit.MINUTES); public FileStore(File storage, PasswordHasher passwordHasher) throws IOException { this.storageFile = storage.toPath(); @@ -33,6 +37,47 @@ public class FileStore implements UserService { json = new JSONObject(Files.readString(storageFile)); } + private FileStore save() { + try { + Files.writeString(storageFile, json.toString(2)); + return this; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /*** User Service Methods ***/ + + + @Override + public UserService delete(User user) { + return null; + } + + @Override + public FileStore init(User defaultUser) { + if (!json.has(SESSIONS)) json.put(SESSIONS, new JSONObject()); + if (!json.has(USERS)) save(defaultUser); + return this; + } + + @Override + public List list() { + return List.of(); + } + + + @Override + public Optional load(String userId) { + try { + var users = json.getJSONObject(USERS); + var user = users.getJSONObject(userId); + return Optional.of(new User(user.getString(USERNAME), user.getString(PASSWORD), user.getString(REALNAME), user.getString(EMAIL), userId)); + } catch (Exception ignored) { + } + return Optional.empty(); + } + @Override public Optional load(String username, String password) { try { @@ -53,35 +98,59 @@ public class FileStore implements UserService { } @Override - public UserService delete(User user) { + public FileStore save(User user) { + JSONObject users; + if (!json.has(USERS)) { + json.put(USERS, users = new JSONObject()); + } else { + users = json.getJSONObject(USERS); + } + users.put(user.uuid(), user.map(true)); + return save(); + } + + /*** Session Service Methods ***/ + + @Override + public Session createSession(User user) { + var now = Instant.now(); + var endOfSession = now.plus(sessionDuration); + return save(new Session(user, endOfSession, UUID.randomUUID().toString())); + } + + @Override + public SessionService dropSession(String sessionId) { return null; } @Override - public UserService init(User defaultUser) { - if (!json.has(USERS)) save(defaultUser); - return this; + public Session extend(String sessionId) { + return null; } @Override - public UserService save(User user) { - JSONObject users; - if (!json.has(USERS)) { - json.put(USERS, users = new JSONObject()); - } else - users = json.getJSONObject(USERS); - users.put(user.uuid(), user.map(true)); + public Optional retrieve(String sessionId) { + var sessions = json.getJSONObject(SESSIONS); try { - Files.writeString(storageFile, json.toString(2)); - } catch (IOException e) { - throw new RuntimeException(e); + var session = sessions.getJSONObject(sessionId); + var userId = session.getString(USER); + var expiration = Instant.ofEpochSecond(session.getLong(EXPIRATION)); + if (expiration.isAfter(Instant.now())) { + return load(userId).map(user -> new Session(user, expiration, sessionId)); + } + } catch (Exception ignored) { } - return this; + return Optional.empty(); } + private Session save(Session session) { + json.getJSONObject(SESSIONS).put(session.id(), Map.of(USER, session.user().uuid(), EXPIRATION, session.expiration().getEpochSecond())); + save(); + return session; + } @Override - public List list() { - return List.of(); + public SessionService setDuration(Duration duration) { + return null; } }