Browse Source

started to implement sessions

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
sqlite
Stephan Richter 4 months ago
parent
commit
59075db1ad
  1. 5
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Cookie.java
  2. 7
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Session.java
  3. 13
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionService.java
  4. 18
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionToken.java
  5. 11
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/UserService.java
  6. 17
      de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java
  7. 31
      de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/Backend.java
  8. 117
      de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java

5
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.Headers;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
import java.util.List;
import java.util.Map; import java.util.Map;
public abstract class Cookie implements Map.Entry<String, String> { public abstract class Cookie implements Map.Entry<String, String> {
@ -33,6 +34,10 @@ public abstract class Cookie implements Map.Entry<String, String> {
return value; return value;
} }
protected static List<String> of(HttpExchange ex) {
return ex.getRequestHeaders().get("Cookie");
}
@Override @Override
public String setValue(String s) { public String setValue(String s) {
var oldVal = value; var oldVal = value;

7
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) {
}

13
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<Session> retrieve(String sessionId);
SessionService setDuration(Duration duration);
}

18
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionToken.java

@ -2,8 +2,22 @@
package de.srsoftware.oidc.api; package de.srsoftware.oidc.api;
import com.sun.net.httpserver.HttpExchange;
import java.util.Optional;
public class SessionToken extends Cookie { public class SessionToken extends Cookie {
public SessionToken(String value) { private final String sessionId;
super("sessionToken", value);
public SessionToken(String sessionId) {
super("sessionToken", sessionId);
this.sessionId = sessionId;
}
public static Optional<SessionToken> 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;
} }
} }

11
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; import java.util.Optional;
public interface UserService { public interface UserService {
public UserService delete(User user); public UserService delete(User user);
public UserService init(User defaultUser); public UserService init(User defaultUser);
public List<User> list(); public List<User> list();
public Optional<User> load(String username, String password); public Optional<User> load(String id);
public UserService save(User user); public Optional<User> load(String username, String password);
public <T extends UserService> T save(User user);
} }

17
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 com.sun.net.httpserver.HttpServer;
import de.srsoftware.oidc.api.SessionService;
import de.srsoftware.oidc.api.User; import de.srsoftware.oidc.api.User;
import de.srsoftware.oidc.api.UserService; import de.srsoftware.oidc.api.UserService;
import de.srsoftware.oidc.backend.Backend; import de.srsoftware.oidc.backend.Backend;
@ -23,15 +24,17 @@ public class Application {
public static final String INDEX = STATIC_PATH + "/index.html"; public static final String INDEX = STATIC_PATH + "/index.html";
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
var storageFile = new File("/tmp/lightoidc.json"); var storageFile = new File("/tmp/lightoidc.json");
var passwordHasher = new UuidHasher(); var passwordHasher = new UuidHasher();
var firstHash = passwordHasher.hash(FIRST_USER_PASS, FIRST_UUID); 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); var firstUser = new User(FIRST_USER, firstHash, FIRST_USER, "%s@internal".formatted(FIRST_USER), FIRST_UUID);
UserService userService = new FileStore(storageFile, passwordHasher).init(firstUser); FileStore fileStore = new FileStore(storageFile, passwordHasher).init(firstUser);
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); UserService userService = fileStore;
SessionService sessionService = fileStore;
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
new StaticPages().bindPath(STATIC_PATH).on(server); new StaticPages().bindPath(STATIC_PATH).on(server);
new Forward(INDEX).bindPath("/").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.setExecutor(Executors.newCachedThreadPool());
server.start(); server.start();
} }

31
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_NOT_FOUND;
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.jar.Attributes.Name.CONTENT_TYPE;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.oidc.api.PathHandler; import de.srsoftware.oidc.api.*;
import de.srsoftware.oidc.api.SessionToken;
import de.srsoftware.oidc.api.User;
import de.srsoftware.oidc.api.UserService;
import java.io.IOException; import java.io.IOException;
import java.util.Optional; import java.util.Optional;
import org.json.JSONObject; import org.json.JSONObject;
public class Backend extends PathHandler { public class Backend extends PathHandler {
private final UserService users; private final SessionService sessions;
private final UserService users;
public Backend(UserService userService) { public Backend(SessionService sessionService, UserService userService) {
users = userService; sessions = sessionService;
users = userService;
} }
private void doLogin(HttpExchange ex) throws IOException { private void doLogin(HttpExchange ex) throws IOException {
@ -32,7 +30,8 @@ public class Backend extends PathHandler {
Optional<User> user = users.load(username, password); Optional<User> user = users.load(username, password);
if (user.isPresent()) { if (user.isPresent()) {
sendUserAndCookie(ex, user.get()); var session = sessions.createSession(user.get());
sendUserAndCookie(ex, session);
return; return;
} }
sendEmptyResponse(HTTP_UNAUTHORIZED, ex); sendEmptyResponse(HTTP_UNAUTHORIZED, ex);
@ -44,12 +43,12 @@ public class Backend extends PathHandler {
String method = ex.getRequestMethod(); String method = ex.getRequestMethod();
System.out.printf("%s %s…", method, path); System.out.printf("%s %s…", method, path);
var user = getSession(ex).map(Session::user);
if ("login".equals(path) && POST.equals(method)) { if ("login".equals(path) && POST.equals(method)) {
doLogin(ex); // TODO: prevent brute force doLogin(ex); // TODO: prevent brute force
return; return;
} }
var token = getAuthToken(ex); if (user.isEmpty()) {
if (token.isEmpty()) {
sendEmptyResponse(HTTP_UNAUTHORIZED, ex); sendEmptyResponse(HTTP_UNAUTHORIZED, ex);
System.err.println("unauthorized"); System.err.println("unauthorized");
return; return;
@ -59,12 +58,16 @@ public class Backend extends PathHandler {
ex.getResponseBody().close(); ex.getResponseBody().close();
} }
private void sendUserAndCookie(HttpExchange ex, User user) throws IOException { private Optional<Session> getSession(HttpExchange ex) {
var bytes = new JSONObject(user.map(false)).toString().getBytes(UTF_8); 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(); var headers = ex.getResponseHeaders();
headers.add(CONTENT_TYPE, JSON); headers.add(CONTENT_TYPE, JSON);
new SessionToken("Test").addTo(headers); new SessionToken(session.id()).addTo(headers);
ex.sendResponseHeaders(200, bytes.length); ex.sendResponseHeaders(200, bytes.length);
ex.getResponseBody().write(bytes); ex.getResponseBody().write(bytes);
} }

117
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 */ package de.srsoftware.oidc.datastore.file; /* © SRSoftware 2024 */
import static de.srsoftware.oidc.api.User.*; import static de.srsoftware.oidc.api.User.*;
import de.srsoftware.oidc.api.PasswordHasher; import de.srsoftware.oidc.api.*;
import de.srsoftware.oidc.api.User;
import de.srsoftware.oidc.api.UserService;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List; import java.time.Duration;
import java.util.Optional; import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import org.json.JSONObject; import org.json.JSONObject;
public class FileStore implements UserService { public class FileStore implements SessionService, UserService {
private static final String USERS = "users"; 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 Path storageFile;
private final JSONObject json; private final JSONObject json;
private final PasswordHasher<String> passwordHasher; private final PasswordHasher<String> passwordHasher;
private Duration sessionDuration = Duration.of(10, ChronoUnit.MINUTES);
public FileStore(File storage, PasswordHasher<String> passwordHasher) throws IOException { public FileStore(File storage, PasswordHasher<String> passwordHasher) throws IOException {
this.storageFile = storage.toPath(); this.storageFile = storage.toPath();
@ -33,6 +37,47 @@ public class FileStore implements UserService {
json = new JSONObject(Files.readString(storageFile)); 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<User> list() {
return List.of();
}
@Override
public Optional<User> 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 @Override
public Optional<User> load(String username, String password) { public Optional<User> load(String username, String password) {
try { try {
@ -53,35 +98,59 @@ public class FileStore implements UserService {
} }
@Override @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; return null;
} }
@Override @Override
public UserService init(User defaultUser) { public Session extend(String sessionId) {
if (!json.has(USERS)) save(defaultUser); return null;
return this;
} }
@Override @Override
public UserService save(User user) { public Optional<Session> retrieve(String sessionId) {
JSONObject users; var sessions = json.getJSONObject(SESSIONS);
if (!json.has(USERS)) {
json.put(USERS, users = new JSONObject());
} else
users = json.getJSONObject(USERS);
users.put(user.uuid(), user.map(true));
try { try {
Files.writeString(storageFile, json.toString(2)); var session = sessions.getJSONObject(sessionId);
} catch (IOException e) { var userId = session.getString(USER);
throw new RuntimeException(e); 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 @Override
public List<User> list() { public SessionService setDuration(Duration duration) {
return List.of(); return null;
} }
} }

Loading…
Cancel
Save