From 0c1baee8e00d2de9dc3f3c44b125f1a7c09109d6 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Thu, 25 Jul 2024 01:26:23 +0200 Subject: [PATCH] divided Backend into several controllers Signed-off-by: Stephan Richter --- .../java/de/srsoftware/cookies/Cookie.java | 14 +- .../de/srsoftware/cookies/SessionToken.java | 10 +- .../de/srsoftware/logging/ColorLogger.java | 20 +- .../de/srsoftware/oidc/api/PathHandler.java | 26 +- .../de/srsoftware/oidc/app/Application.java | 38 ++- .../de/srsoftware/oidc/backend/Backend.java | 244 ------------------ .../oidc/backend/ClientController.java | 126 +++++++++ .../srsoftware/oidc/backend/Controller.java | 21 ++ .../oidc/backend/TokenController.java | 41 +++ .../oidc/backend/UserController.java | 114 ++++++++ .../oidc/backend/WellKnownController.java | 27 ++ .../src/main/resources/en/authorization.js | 2 +- .../src/main/resources/en/clients.js | 4 +- .../src/main/resources/en/common.js | 2 + .../src/main/resources/en/login.js | 2 +- .../src/main/resources/en/logout.js | 2 +- .../src/main/resources/en/newclient.js | 2 +- .../src/main/resources/en/settings.js | 4 +- .../src/main/resources/en/user.js | 2 +- 19 files changed, 410 insertions(+), 291 deletions(-) delete mode 100644 de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/Backend.java create mode 100644 de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/ClientController.java create mode 100644 de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/Controller.java create mode 100644 de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/TokenController.java create mode 100644 de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java create mode 100644 de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/WellKnownController.java diff --git a/de.srsoftware.cookies/src/main/java/de/srsoftware/cookies/Cookie.java b/de.srsoftware.cookies/src/main/java/de/srsoftware/cookies/Cookie.java index 1285d10..fba79a7 100644 --- a/de.srsoftware.cookies/src/main/java/de/srsoftware/cookies/Cookie.java +++ b/de.srsoftware.cookies/src/main/java/de/srsoftware/cookies/Cookie.java @@ -8,7 +8,11 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import static java.lang.System.Logger.Level.ERROR; +import static java.lang.System.Logger.Level.WARNING; + public abstract class Cookie implements Map.Entry { + static final System.Logger LOG = System.getLogger(SessionToken.class.getSimpleName()); private final String key; private String value = null; @@ -18,6 +22,7 @@ public abstract class Cookie implements Map.Entry { } public T addTo(Headers headers) { + LOG.log(ERROR,"sending cookie {0}={1}",key,value); headers.add("Set-Cookie", "%s=%s".formatted(key, value)); return (T)this; } @@ -37,7 +42,14 @@ public abstract class Cookie implements Map.Entry { } protected static List of(HttpExchange ex) { - return Optional.ofNullable(ex.getRequestHeaders().get("Cookie")).stream().flatMap(List::stream).flatMap(s -> Arrays.stream(s.split(";"))).map(String::trim).toList(); + return Optional.ofNullable(ex.getRequestHeaders() + .get("Cookie")) + .stream() + .flatMap(List::stream) + .flatMap(s -> Arrays.stream(s.split(";"))) + .map(String::trim) + .peek(cookie -> LOG.log(WARNING,"received cookie {0}",cookie)) + .toList(); } @Override diff --git a/de.srsoftware.cookies/src/main/java/de/srsoftware/cookies/SessionToken.java b/de.srsoftware.cookies/src/main/java/de/srsoftware/cookies/SessionToken.java index 05c54c8..c99b4a9 100644 --- a/de.srsoftware.cookies/src/main/java/de/srsoftware/cookies/SessionToken.java +++ b/de.srsoftware.cookies/src/main/java/de/srsoftware/cookies/SessionToken.java @@ -4,17 +4,23 @@ package de.srsoftware.cookies; import com.sun.net.httpserver.HttpExchange; import java.util.Optional; +import java.util.logging.Logger; + +import static java.lang.System.Logger.Level.DEBUG; +import static java.lang.System.Logger.Level.WARNING; public class SessionToken extends Cookie { private final String sessionId; public SessionToken(String sessionId) { - super("sessionToken", sessionId); + super("sessionToken", sessionId+"; Path=/api"); 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(); + 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() { diff --git a/de.srsoftware.logging/src/main/java/de/srsoftware/logging/ColorLogger.java b/de.srsoftware.logging/src/main/java/de/srsoftware/logging/ColorLogger.java index 04ac46b..6e9ba85 100644 --- a/de.srsoftware.logging/src/main/java/de/srsoftware/logging/ColorLogger.java +++ b/de.srsoftware.logging/src/main/java/de/srsoftware/logging/ColorLogger.java @@ -11,11 +11,11 @@ import java.util.Date; import java.util.ResourceBundle; public class ColorLogger implements System.Logger { - private final String name; - private static int rootLevel = INFO.getSeverity(); - private static DateFormat TIME = new SimpleDateFormat("hh:mm:ss.SSS"); - private static DateFormat DATE = new SimpleDateFormat("yyyy-MM-dd"); - private static String lastDate = null; + private final String name; + private static int rootLevel = INFO.getSeverity(); + private static DateFormat TIME = new SimpleDateFormat("hh:mm:ss.SSS"); + private static DateFormat DATE = new SimpleDateFormat("yyyy-MM-dd"); + private static String lastDate = null; public ColorLogger(String name) { this.name = name; @@ -52,11 +52,11 @@ public class ColorLogger implements System.Logger { } private static String colorize(String message, int severity) { - var color = severity >= ERROR.getSeverity() ? RED : severity >= WARNING.getSeverity() ? YELLOW : severity >= INFO.getSeverity() ? WHITE_BRIGHT : WHITE; - var date = new Date(); - var day =DATE.format(date); - StringBuilder sb = new StringBuilder(); - if (!day.equals(lastDate)){ + var color = severity >= ERROR.getSeverity() ? RED : severity >= WARNING.getSeverity() ? YELLOW : severity >= INFO.getSeverity() ? WHITE_BRIGHT : WHITE; + var date = new Date(); + var day = DATE.format(date); + StringBuilder sb = new StringBuilder(); + if (!day.equals(lastDate)) { lastDate = day; sb.append(WHITE).append(day).append("\n"); } diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/PathHandler.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/PathHandler.java index 112c288..f3ce0b1 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/PathHandler.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/PathHandler.java @@ -1,8 +1,7 @@ /* © SRSoftware 2024 */ package de.srsoftware.oidc.api; -import static java.lang.System.Logger.Level.DEBUG; -import static java.lang.System.Logger.Level.INFO; +import static java.lang.System.Logger.Level.*; import static java.net.HttpURLConnection.*; import static java.nio.charset.StandardCharsets.UTF_8; @@ -18,14 +17,14 @@ import java.util.stream.Stream; import org.json.JSONObject; public abstract class PathHandler implements HttpHandler { - public static final String CONTENT_TYPE = "Content-Type"; - public static final String DELETE = "DELETE"; + public static final String CONTENT_TYPE = "Content-Type"; + public static final String DELETE = "DELETE"; private static final String FORWARDED_HOST = "x-forwarded-host"; - public static final String GET = "GET"; - public static final String HOST = "host"; - public static final String JSON = "application/json"; - public static System.Logger LOG = System.getLogger(PathHandler.class.getSimpleName()); - public static final String POST = "POST"; + public static final String GET = "GET"; + public static final String HOST = "host"; + public static final String JSON = "application/json"; + public static System.Logger LOG = System.getLogger(PathHandler.class.getSimpleName()); + public static final String POST = "POST"; private String[] paths; @@ -99,9 +98,9 @@ public abstract class PathHandler implements HttpHandler { public static String hostname(HttpExchange ex) { var headers = ex.getRequestHeaders(); - var host = headers.getFirst(FORWARDED_HOST); + var host = headers.getFirst(FORWARDED_HOST); if (host == null) host = headers.getFirst(HOST); - return host == null ? null : "https://"+host; + return host == null ? null : "https://" + host; } public static JSONObject json(HttpExchange ex) throws IOException { @@ -112,6 +111,11 @@ public abstract class PathHandler implements HttpHandler { return getHeader(ex, "Accept-Language").map(s -> Arrays.stream(s.split(","))).flatMap(Stream::findFirst); } + public static boolean notFound(HttpExchange ex) throws IOException { + LOG.log(WARNING, "not implemented"); + return sendEmptyResponse(HTTP_NOT_FOUND, ex); + } + public static boolean sendEmptyResponse(int statusCode, HttpExchange ex) throws IOException { ex.sendResponseHeaders(statusCode, 0); return false; 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 e2d83a0..829a525 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 @@ -9,7 +9,10 @@ import static java.lang.System.Logger.Level.ERROR; import com.sun.net.httpserver.HttpServer; import de.srsoftware.logging.ColorLogger; import de.srsoftware.oidc.api.User; -import de.srsoftware.oidc.backend.Backend; +import de.srsoftware.oidc.backend.ClientController; +import de.srsoftware.oidc.backend.TokenController; +import de.srsoftware.oidc.backend.UserController; +import de.srsoftware.oidc.backend.WellKnownController; import de.srsoftware.oidc.datastore.file.FileStore; import de.srsoftware.oidc.datastore.file.UuidHasher; import de.srsoftware.oidc.web.Forward; @@ -21,17 +24,20 @@ import java.util.*; import java.util.concurrent.Executors; public class Application { - public static final String BACKEND = "/api"; - private static final String FAVICON = "/favicon.ico"; - public static final String ROOT = "/"; - public static final String STATIC_PATH = "/web"; - private static final String WELL_KNOWN = "/.well-known"; - public static final String FIRST_USER = "admin"; - public static final String FIRST_USER_PASS = "admin"; - public static final String FIRST_UUID = UUID.randomUUID().toString(); - public static final String INDEX = STATIC_PATH + "/index.html"; - private static final String BASE_PATH = "basePath"; - private static System.Logger LOG = new ColorLogger("Application").setLogLevel(DEBUG); + public static final String API_CLIENT = "/api/client"; + private static final String API_TOKEN = "/api/token"; + public static final String API_USER = "/api/user"; + public static final String FIRST_USER = "admin"; + public static final String FIRST_USER_PASS = "admin"; + public static final String FIRST_UUID = UUID.randomUUID().toString(); + public static final String ROOT = "/"; + public static final String STATIC_PATH = "/web"; + + private static final String BASE_PATH = "basePath"; + private static final String FAVICON = "/favicon.ico"; + private static final String INDEX = STATIC_PATH + "/index.html"; + private static final String WELL_KNOWN = "/.well-known"; + private static System.Logger LOG = new ColorLogger("Application").setLogLevel(DEBUG); public static void main(String[] args) throws Exception { var argMap = map(args); @@ -44,8 +50,12 @@ public class Application { HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); new StaticPages(basePath).bindPath(STATIC_PATH, FAVICON).on(server); new Forward(INDEX).bindPath(ROOT).on(server); - new Backend(fileStore, fileStore, fileStore, fileStore).bindPath(BACKEND, WELL_KNOWN).on(server); - server.setExecutor(Executors.newCachedThreadPool()); + new WellKnownController().bindPath(WELL_KNOWN).on(server); + new UserController(fileStore, fileStore).bindPath(API_USER).on(server); + new TokenController().bindPath(API_TOKEN).on(server); + new ClientController(fileStore, fileStore, fileStore).bindPath(API_CLIENT).on(server); + //server.setExecutor(Executors.newCachedThreadPool()); + server.setExecutor(Executors.newSingleThreadExecutor()); 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 deleted file mode 100644 index 3f5a5bd..0000000 --- a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/Backend.java +++ /dev/null @@ -1,244 +0,0 @@ -/* © SRSoftware 2024 */ -package de.srsoftware.oidc.backend; - -import static de.srsoftware.oidc.api.Constants.*; -import static de.srsoftware.oidc.api.Permission.MANAGE_CLIENTS; -import static de.srsoftware.oidc.api.User.*; -import static java.lang.System.Logger.Level.ERROR; -import static java.lang.System.Logger.Level.WARNING; -import static java.net.HttpURLConnection.*; - -import com.sun.net.httpserver.HttpExchange; -import de.srsoftware.cookies.SessionToken; -import de.srsoftware.oidc.api.*; -import java.io.IOException; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; -import org.json.JSONObject; - -public class Backend extends PathHandler { - private static final System.Logger LOG = System.getLogger(Backend.class.getSimpleName()); - private final AuthorizationService authorizations; - private final SessionService sessions; - private final UserService users; - private final ClientService clients; - - public Backend(AuthorizationService authorizationService, ClientService clientService, SessionService sessionService, UserService userService) { - authorizations = authorizationService; - clients = clientService; - sessions = sessionService; - users = userService; - } - - private boolean addClient(HttpExchange ex, Session session) throws IOException { - if (!session.user().hasPermission(MANAGE_CLIENTS)) return badRequest(ex, "NOT ALLOWED"); - var json = json(ex); - var redirects = new HashSet(); - for (Object o : json.getJSONArray(REDIRECT_URIS)) { - if (o instanceof String s) redirects.add(s); - } - var client = new Client(json.getString(CLIENT_ID), json.getString(NAME), json.getString(SECRET), redirects); - clients.add(client); - return sendContent(ex, client); - } - - private boolean authorize(HttpExchange ex, Session session) throws IOException { - var user = session.user(); - var json = json(ex); - var clientId = json.getString(CLIENT_ID); - var redirect = json.getString(REDIRECT_URI); - var optClient = clients.getClient(clientId); - if (optClient.isEmpty()) return badRequest(ex, Map.of(CAUSE, "unknown client", CLIENT_ID, clientId)); - var client = optClient.get(); - - if (!client.redirectUris().contains(redirect)) return badRequest(ex, Map.of(CAUSE, "unknown redirect uri", REDIRECT_URI, redirect)); - - if (!authorizations.isAuthorized(client, session.user())) { - if (json.has(CONFIRMED) && json.getBoolean(CONFIRMED)) { - authorizations.authorize(client, user, null); - } else { - return sendContent(ex, Map.of(CONFIRMED, false, NAME, client.name())); - } - } - var state = json.getString(STATE); - var code = client.generateCode(); - return sendContent(ex, Map.of(CONFIRMED, true, CODE, code, REDIRECT_URI, redirect, STATE, state)); - } - - private boolean clients(HttpExchange ex, Session session) throws IOException { - var user = session.user(); - if (!user.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); - } - - private boolean deleteClient(HttpExchange ex, Session session) throws IOException { - if (!session.user().hasPermission(MANAGE_CLIENTS)) return badRequest(ex, "NOT ALLOWED"); - var json = json(ex); - var id = json.getString(CLIENT_ID); - clients.getClient(id).ifPresent(clients::remove); - return sendEmptyResponse(HTTP_OK, ex); - } - - private boolean doLogin(HttpExchange ex) throws IOException { - var body = json(ex); - - var username = body.has(USERNAME) ? body.getString(USERNAME) : null; - var password = body.has(PASSWORD) ? body.getString(PASSWORD) : null; - - Optional user = users.load(username, password); - if (user.isPresent()) return sendUserAndCookie(ex, sessions.createSession(user.get())); - return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); - } - - @Override - 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 session = optSession.get(); - switch (path) { - case "/client": - return deleteClient(ex, session); - } - LOG.log(ERROR, "not implemented"); - return sendEmptyResponse(HTTP_NOT_FOUND, ex); - } - - @Override - public boolean doGet(String path, HttpExchange ex) throws IOException { - // pre-login paths - switch (path) { - case "/openid-configuration": - return openidConfig(ex); - } - - var optSession = getSession(ex); - if (optSession.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); - - // post-login paths - var session = optSession.get(); - switch (path) { - case "/logout": - return logout(ex, session); - } - - LOG.log(WARNING, "not implemented"); - return sendEmptyResponse(HTTP_NOT_FOUND, ex); - } - - @Override - public boolean doPost(String path, HttpExchange ex) throws IOException { - // pre-login paths - switch (path) { - case "/login": - return doLogin(ex); - case "/token": - return provideToken(ex); - } - var optSession = getSession(ex); - if (optSession.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); - - // post-login paths - var session = optSession.get(); - switch (path) { - case "/add/client": - return addClient(ex, session); - case "/authorize": - return authorize(ex, session); - case "/client": - return loadClient(ex, session); - case "/clients": - return clients(ex, session); - case "/update/password": - return updatePassword(ex, session); - case "/update/user": - return updateUser(ex, session); - case "/user": - return sendUserAndCookie(ex, session); - } - LOG.log(WARNING, "not implemented"); - return sendEmptyResponse(HTTP_NOT_FOUND, ex); - } - - private Optional getSession(HttpExchange ex) { - return SessionToken.from(ex).map(SessionToken::sessionId).flatMap(sessions::retrieve); - } - - private boolean loadClient(HttpExchange ex, Session session) throws IOException { - if (!session.user().hasPermission(MANAGE_CLIENTS)) return sendEmptyResponse(HTTP_FORBIDDEN, ex); - var json = json(ex); - if (json.has(CLIENT_ID)) { - var clientID = json.getString(CLIENT_ID); - var client = clients.getClient(clientID).map(Client::map).map(JSONObject::new); - if (client.isPresent()) return sendContent(ex, client.get()); - } - return sendEmptyResponse(HTTP_NOT_FOUND, ex); - } - - private boolean logout(HttpExchange ex, Session session) throws IOException { - sessions.dropSession(session.id()); - new SessionToken("").addTo(ex); - return sendEmptyResponse(HTTP_OK, ex); - } - - private boolean provideToken(HttpExchange ex) throws IOException { - var map = deserialize(body(ex)); - LOG.log(WARNING, "map: {0}", map); - LOG.log(ERROR, "{0}.provideToken(ex) not implemented!", getClass().getSimpleName()); - var grantType = map.get(GRANT_TYPE); - if (!ATUH_CODE.equals(grantType)) sendContent(ex, HTTP_BAD_REQUEST, Map.of(ERROR, "unknown grant type", GRANT_TYPE, grantType)); - return sendEmptyResponse(HTTP_NOT_FOUND, ex); - } - - private Map deserialize(String body) { - return Arrays.stream(body.split("&")).map(s -> s.split("=")).collect(Collectors.toMap(arr -> arr[0], arr -> arr[1])); - } - - private boolean openidConfig(HttpExchange ex) throws IOException { - var host = hostname(ex); - return sendContent(ex, Map.of("token_endpoint", host + "/api/token", "authorization_endpoint", host + "/web/authorization.html", "userinfo_endpoint", host + "/api/userinfo", "jwks_uri", host + "/api/jwks")); - } - - private boolean sendUserAndCookie(HttpExchange ex, Session session) throws IOException { - new SessionToken(session.id()).addTo(ex); - return sendContent(ex, session.user().map(false)); - } - - private boolean updatePassword(HttpExchange ex, Session session) throws IOException { - var user = session.user(); - var json = json(ex); - var uuid = json.getString(UUID); - if (!uuid.equals(user.uuid())) { - return sendEmptyResponse(HTTP_FORBIDDEN, ex); - } - var oldPass = json.getString("oldpass"); - if (!users.passwordMatches(oldPass, user.hashedPassword())) return badRequest(ex, "wrong password"); - - var newpass = json.getJSONArray("newpass"); - var newPass1 = newpass.getString(0); - if (!newPass1.equals(newpass.getString(1))) { - return badRequest(ex, "password mismatch"); - } - users.updatePassword(user, newPass1); - return sendContent(ex, user.map(false)); - } - - private boolean updateUser(HttpExchange ex, Session session) throws IOException { - var user = session.user(); - var json = json(ex); - var uuid = json.getString(UUID); - if (!uuid.equals(user.uuid())) { - return sendEmptyResponse(HTTP_FORBIDDEN, ex); - } - user.username(json.getString(USERNAME)); - user.email(json.getString(EMAIL)); - users.save(user); - return sendContent(ex, user.map(false)); - } -} diff --git a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/ClientController.java b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/ClientController.java new file mode 100644 index 0000000..6fb292a --- /dev/null +++ b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/ClientController.java @@ -0,0 +1,126 @@ +/* © SRSoftware 2024 */ +package de.srsoftware.oidc.backend; + +import static de.srsoftware.oidc.api.Constants.*; +import static de.srsoftware.oidc.api.Permission.MANAGE_CLIENTS; +import static java.lang.System.Logger.Level.ERROR; +import static java.net.HttpURLConnection.*; + +import com.sun.net.httpserver.HttpExchange; +import de.srsoftware.oidc.api.*; +import java.io.IOException; +import java.util.HashSet; +import java.util.Map; +import org.json.JSONObject; + +public class ClientController extends Controller { + private static final System.Logger LOG = System.getLogger(ClientController.class.getSimpleName()); + private final AuthorizationService authorizations; + private final ClientService clients; + + public ClientController(AuthorizationService authorizationService, ClientService clientService, SessionService sessionService) { + super(sessionService); + authorizations = authorizationService; + clients = clientService; + } + + private boolean add(HttpExchange ex, Session session) throws IOException { + if (!session.user().hasPermission(MANAGE_CLIENTS)) return badRequest(ex, "NOT ALLOWED"); + var json = json(ex); + var redirects = new HashSet(); + for (Object o : json.getJSONArray(REDIRECT_URIS)) { + if (o instanceof String s) redirects.add(s); + } + var client = new Client(json.getString(CLIENT_ID), json.getString(NAME), json.getString(SECRET), redirects); + clients.add(client); + return sendContent(ex, client); + } + + private boolean authorize(HttpExchange ex, Session session) throws IOException { + var user = session.user(); + var json = json(ex); + var clientId = json.getString(CLIENT_ID); + var redirect = json.getString(REDIRECT_URI); + var optClient = clients.getClient(clientId); + if (optClient.isEmpty()) return badRequest(ex, Map.of(CAUSE, "unknown client", CLIENT_ID, clientId)); + var client = optClient.get(); + + if (!client.redirectUris().contains(redirect)) return badRequest(ex, Map.of(CAUSE, "unknown redirect uri", REDIRECT_URI, redirect)); + + if (!authorizations.isAuthorized(client, session.user())) { + if (json.has(CONFIRMED) && json.getBoolean(CONFIRMED)) { + authorizations.authorize(client, user, null); + } else { + return sendContent(ex, Map.of(CONFIRMED, false, NAME, client.name())); + } + } + var state = json.getString(STATE); + var code = client.generateCode(); + return sendContent(ex, Map.of(CONFIRMED, true, CODE, code, REDIRECT_URI, redirect, STATE, state)); + } + + private boolean deleteClient(HttpExchange ex, Session session) throws IOException { + if (!session.user().hasPermission(MANAGE_CLIENTS)) return badRequest(ex, "NOT ALLOWED"); + var json = json(ex); + var id = json.getString(CLIENT_ID); + clients.getClient(id).ifPresent(clients::remove); + return sendEmptyResponse(HTTP_OK, ex); + } + + + @Override + 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 session = optSession.get(); + switch (path) { + case "/": + return deleteClient(ex, session); + } + LOG.log(ERROR, "not implemented"); + return sendEmptyResponse(HTTP_NOT_FOUND, ex); + } + + + @Override + public boolean doPost(String path, HttpExchange ex) throws IOException { + var optSession = getSession(ex); + if (optSession.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); + + // post-login paths + var session = optSession.get(); + switch (path) { + case "/": + return load(ex, session); + case "/add": + return add(ex, session); + case "/authorize": + return authorize(ex, session); + case "/list": + return list(ex, session); + } + return notFound(ex); + } + + private boolean list(HttpExchange ex, Session session) throws IOException { + var user = session.user(); + if (!user.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); + } + + + private boolean load(HttpExchange ex, Session session) throws IOException { + if (!session.user().hasPermission(MANAGE_CLIENTS)) return sendEmptyResponse(HTTP_FORBIDDEN, ex); + var json = json(ex); + if (json.has(CLIENT_ID)) { + var clientID = json.getString(CLIENT_ID); + var client = clients.getClient(clientID).map(Client::map).map(JSONObject::new); + if (client.isPresent()) return sendContent(ex, client.get()); + } + return sendEmptyResponse(HTTP_NOT_FOUND, ex); + } +} diff --git a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/Controller.java b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/Controller.java new file mode 100644 index 0000000..2fcdd9b --- /dev/null +++ b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/Controller.java @@ -0,0 +1,21 @@ +/* © SRSoftware 2024 */ +package de.srsoftware.oidc.backend; + +import com.sun.net.httpserver.HttpExchange; +import de.srsoftware.cookies.SessionToken; +import de.srsoftware.oidc.api.PathHandler; +import de.srsoftware.oidc.api.Session; +import de.srsoftware.oidc.api.SessionService; +import java.util.Optional; + +public abstract class Controller extends PathHandler { + protected final SessionService sessions; + + Controller(SessionService sessionService) { + sessions = sessionService; + } + + protected Optional getSession(HttpExchange ex) { + return SessionToken.from(ex).map(SessionToken::sessionId).flatMap(sessions::retrieve); + } +} diff --git a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/TokenController.java b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/TokenController.java new file mode 100644 index 0000000..6d3161c --- /dev/null +++ b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/TokenController.java @@ -0,0 +1,41 @@ +/* © SRSoftware 2024 */ +package de.srsoftware.oidc.backend; + +import static de.srsoftware.oidc.api.Constants.ATUH_CODE; +import static de.srsoftware.oidc.api.Constants.GRANT_TYPE; +import static java.lang.System.Logger.Level.ERROR; +import static java.lang.System.Logger.Level.WARNING; +import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; +import static java.net.HttpURLConnection.HTTP_NOT_FOUND; + +import com.sun.net.httpserver.HttpExchange; +import de.srsoftware.oidc.api.PathHandler; +import java.io.IOException; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +public class TokenController extends PathHandler { + private Map deserialize(String body) { + return Arrays.stream(body.split("&")).map(s -> s.split("=")).collect(Collectors.toMap(arr -> arr[0], arr -> arr[1])); + } + + @Override + public boolean doPost(String path, HttpExchange ex) throws IOException { + // pre-login paths + switch (path) { + case "/": + return provideToken(ex); + } + return notFound(ex); + } + + private boolean provideToken(HttpExchange ex) throws IOException { + var map = deserialize(body(ex)); + LOG.log(WARNING, "post data: {0}", map); + LOG.log(ERROR, "{0}.provideToken(ex) not implemented!", getClass().getSimpleName()); + var grantType = map.get(GRANT_TYPE); + if (!ATUH_CODE.equals(grantType)) sendContent(ex, HTTP_BAD_REQUEST, Map.of(ERROR, "unknown grant type", GRANT_TYPE, grantType)); + return sendEmptyResponse(HTTP_NOT_FOUND, ex); + } +} diff --git a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java new file mode 100644 index 0000000..aa928e2 --- /dev/null +++ b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java @@ -0,0 +1,114 @@ +/* © SRSoftware 2024 */ +package de.srsoftware.oidc.backend; + +import static de.srsoftware.oidc.api.User.*; +import static java.lang.System.Logger.Level.WARNING; +import static java.net.HttpURLConnection.*; + +import com.sun.net.httpserver.HttpExchange; +import de.srsoftware.cookies.SessionToken; +import de.srsoftware.oidc.api.*; +import java.io.IOException; +import java.util.Optional; + +public class UserController extends Controller { + private final UserService users; + + public UserController(SessionService sessionService, UserService userService) { + super(sessionService); + users = userService; + } + + @Override + public boolean doGet(String path, HttpExchange ex) throws IOException { + var optSession = getSession(ex); + if (optSession.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); + + // post-login paths + var session = optSession.get(); + switch (path) { + case "/logout": + return logout(ex, session); + } + + LOG.log(WARNING, "not implemented"); + return sendEmptyResponse(HTTP_NOT_FOUND, ex); + } + + + @Override + public boolean doPost(String path, HttpExchange ex) throws IOException { + switch (path) { + case "/login": + return login(ex); + } + var optSession = getSession(ex); + if (optSession.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); + + // post-login paths + var session = optSession.get(); + switch (path) { + case "/": + return sendUserAndCookie(ex, session); + case "/password": + return updatePassword(ex, session); + case "/update": + return updateUser(ex, session); + } + return notFound(ex); + } + + private boolean login(HttpExchange ex) throws IOException { + var body = json(ex); + + var username = body.has(USERNAME) ? body.getString(USERNAME) : null; + var password = body.has(PASSWORD) ? body.getString(PASSWORD) : null; + + Optional user = users.load(username, password); + if (user.isPresent()) return sendUserAndCookie(ex, sessions.createSession(user.get())); + return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); + } + + private boolean logout(HttpExchange ex, Session session) throws IOException { + sessions.dropSession(session.id()); + new SessionToken("").addTo(ex); + return sendEmptyResponse(HTTP_OK, ex); + } + + private boolean sendUserAndCookie(HttpExchange ex, Session session) throws IOException { + new SessionToken(session.id()).addTo(ex); + return sendContent(ex, session.user().map(false)); + } + + private boolean updatePassword(HttpExchange ex, Session session) throws IOException { + var user = session.user(); + var json = json(ex); + var uuid = json.getString(UUID); + if (!uuid.equals(user.uuid())) { + return sendEmptyResponse(HTTP_FORBIDDEN, ex); + } + var oldPass = json.getString("oldpass"); + if (!users.passwordMatches(oldPass, user.hashedPassword())) return badRequest(ex, "wrong password"); + + var newpass = json.getJSONArray("newpass"); + var newPass1 = newpass.getString(0); + if (!newPass1.equals(newpass.getString(1))) { + return badRequest(ex, "password mismatch"); + } + users.updatePassword(user, newPass1); + return sendContent(ex, user.map(false)); + } + + private boolean updateUser(HttpExchange ex, Session session) throws IOException { + var user = session.user(); + var json = json(ex); + var uuid = json.getString(UUID); + if (!uuid.equals(user.uuid())) { + return sendEmptyResponse(HTTP_FORBIDDEN, ex); + } + user.username(json.getString(USERNAME)); + user.email(json.getString(EMAIL)); + users.save(user); + return sendContent(ex, user.map(false)); + } +} diff --git a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/WellKnownController.java b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/WellKnownController.java new file mode 100644 index 0000000..17b5ee0 --- /dev/null +++ b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/WellKnownController.java @@ -0,0 +1,27 @@ +/* © SRSoftware 2024 */ +package de.srsoftware.oidc.backend; + +import static java.lang.System.Logger.Level.WARNING; +import static java.net.HttpURLConnection.HTTP_NOT_FOUND; + +import com.sun.net.httpserver.HttpExchange; +import de.srsoftware.oidc.api.PathHandler; +import java.io.IOException; +import java.util.Map; + +public class WellKnownController extends PathHandler { + @Override + public boolean doGet(String path, HttpExchange ex) throws IOException { + switch (path) { + case "/openid-configuration": + return openidConfig(ex); + } + LOG.log(WARNING, "not implemented"); + return sendEmptyResponse(HTTP_NOT_FOUND, ex); + } + + private boolean openidConfig(HttpExchange ex) throws IOException { + var host = hostname(ex); + return sendContent(ex, Map.of("token_endpoint", host + "/api/token", "authorization_endpoint", host + "/web/authorization.html", "userinfo_endpoint", host + "/api/userinfo", "jwks_uri", host + "/api/jwks")); + } +} diff --git a/de.srsoftware.oidc.web/src/main/resources/en/authorization.js b/de.srsoftware.oidc.web/src/main/resources/en/authorization.js index 252025c..ce29ea0 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/authorization.js +++ b/de.srsoftware.oidc.web/src/main/resources/en/authorization.js @@ -34,7 +34,7 @@ function denyAutorization(){ } function backendAutorization(){ - fetch(api+"/authorize",{ + fetch(client_controller+"/authorize",{ method: 'POST', body: JSON.stringify(json), headers: { diff --git a/de.srsoftware.oidc.web/src/main/resources/en/clients.js b/de.srsoftware.oidc.web/src/main/resources/en/clients.js index f13471b..9aba0cb 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/clients.js +++ b/de.srsoftware.oidc.web/src/main/resources/en/clients.js @@ -20,7 +20,7 @@ function handleRemove(response){ function remove(clientId){ var message = document.getElementById('message').innerHTML; if (confirm(message.replace("{}",clientId))) { - fetch(api+"/client",{ + fetch(client_controller+"/delete",{ method: 'DELETE', body : JSON.stringify({ client_id : clientId }) }).then(handleRemove); @@ -31,4 +31,4 @@ function edit(clientId){ redirect("edit_client.html?id="+clientId); } -fetch(api+"/clients",{method:'POST'}).then(handleClients); \ No newline at end of file +fetch(client_controller+"/list",{method:'POST'}).then(handleClients); \ No newline at end of file diff --git a/de.srsoftware.oidc.web/src/main/resources/en/common.js b/de.srsoftware.oidc.web/src/main/resources/en/common.js index 3bc151e..f0d8718 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/common.js +++ b/de.srsoftware.oidc.web/src/main/resources/en/common.js @@ -1,4 +1,6 @@ var api = "/api"; +var client_controller = "/api/client" +var user_controller = "/api/user" var web = "/web"; const UNAUTHORIZED = 401; diff --git a/de.srsoftware.oidc.web/src/main/resources/en/login.js b/de.srsoftware.oidc.web/src/main/resources/en/login.js index 7217326..c19d3c0 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/login.js +++ b/de.srsoftware.oidc.web/src/main/resources/en/login.js @@ -17,7 +17,7 @@ function doRedirect(){ function tryLogin(){ var username = getValue('username'); var password = getValue('password'); - fetch(api+"/login",{ + fetch(user_controller+"/login",{ method: 'POST', headers: { Accept: 'application/json', diff --git a/de.srsoftware.oidc.web/src/main/resources/en/logout.js b/de.srsoftware.oidc.web/src/main/resources/en/logout.js index c9075c1..504a9c0 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/logout.js +++ b/de.srsoftware.oidc.web/src/main/resources/en/logout.js @@ -2,4 +2,4 @@ function handleLogout(response){ if (response.ok) document.body.innerHTML += 'success'; redirect('index.html') } -fetch(api+"/logout").then(handleLogout) \ No newline at end of file +fetch(user_controller+"/logout").then(handleLogout) \ No newline at end of file diff --git a/de.srsoftware.oidc.web/src/main/resources/en/newclient.js b/de.srsoftware.oidc.web/src/main/resources/en/newclient.js index 20e9cf6..76b397d 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/newclient.js +++ b/de.srsoftware.oidc.web/src/main/resources/en/newclient.js @@ -6,7 +6,7 @@ function addClient(){ secret : getValue('client-secret'), redirect_uris : getValue('redirect-urls').split("\n") }; - fetch(api+'/add/client',{ + fetch(client_controller+'/add',{ method : 'POST', headers : { 'Content-Type': 'application/json' diff --git a/de.srsoftware.oidc.web/src/main/resources/en/settings.js b/de.srsoftware.oidc.web/src/main/resources/en/settings.js index 1ee2df9..a822343 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/settings.js +++ b/de.srsoftware.oidc.web/src/main/resources/en/settings.js @@ -31,7 +31,7 @@ function update(){ email : getValue('email'), uuid : getValue('uuid') } - fetch(api+'/update/user',{ + fetch(user_controller+'/update',{ method : 'POST', headers : { 'Content-Type': 'application/json' @@ -68,7 +68,7 @@ function updatePass(){ newpass : [getValue('newpass1'),getValue('newpass2')], uuid : getValue('uuid') } - fetch(api+'/update/password',{ + fetch(user_controller+'/password',{ method : 'POST', headers : { 'Content-Type': 'application/json' diff --git a/de.srsoftware.oidc.web/src/main/resources/en/user.js b/de.srsoftware.oidc.web/src/main/resources/en/user.js index d90e49a..63c80ef 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/user.js +++ b/de.srsoftware.oidc.web/src/main/resources/en/user.js @@ -24,4 +24,4 @@ async function handleNavigation(response){ } } -fetch(api+"/user",{method:'POST'}).then(handleUser); \ No newline at end of file +fetch(user_controller+"/",{method:'POST'}).then(handleUser); \ No newline at end of file