started to implement sessions
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -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<String, String> {
|
||||
@@ -33,6 +34,10 @@ public abstract class Cookie implements Map.Entry<String, String> {
|
||||
return value;
|
||||
}
|
||||
|
||||
protected static List<String> of(HttpExchange ex) {
|
||||
return ex.getRequestHeaders().get("Cookie");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String setValue(String s) {
|
||||
var oldVal = value;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
/* © SRSoftware 2024 */
|
||||
package de.srsoftware.oidc.api;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public record Session(User user, Instant expiration, String id) {
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<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;
|
||||
}
|
||||
}
|
||||
@@ -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<User> list();
|
||||
public Optional<User> load(String username, String password);
|
||||
public UserService save(User user);
|
||||
public UserService delete(User user);
|
||||
public UserService init(User defaultUser);
|
||||
public List<User> list();
|
||||
public Optional<User> load(String id);
|
||||
public Optional<User> load(String username, String password);
|
||||
public <T extends UserService> T save(User user);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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> 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<Session> 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);
|
||||
}
|
||||
|
||||
@@ -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<String> passwordHasher;
|
||||
private Duration sessionDuration = Duration.of(10, ChronoUnit.MINUTES);
|
||||
|
||||
public FileStore(File storage, PasswordHasher<String> 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<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
|
||||
public Optional<User> 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<Session> 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<User> list() {
|
||||
return List.of();
|
||||
public SessionService setDuration(Duration duration) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user