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.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;
|
||||||
|
|||||||
@@ -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;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user