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 5ab25ae..83f7aba 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,6 +1,7 @@ /* © SRSoftware 2024 */ package de.srsoftware.oidc.api; +import static java.net.HttpURLConnection.HTTP_OK; import static java.nio.charset.StandardCharsets.UTF_8; import com.sun.net.httpserver.HttpExchange; @@ -15,56 +16,103 @@ import org.json.JSONObject; public abstract class PathHandler implements HttpHandler { public static final String CONTENT_TYPE = "Content-Type"; + public static final String GET = "GET"; public static final String JSON = "application/json"; public static final String POST = "POST"; - private String path; + private String[] paths; public class Bond { - Bond(String p) { - path = p; + Bond(String[] paths) { + PathHandler.this.paths = paths; } public HttpServer on(HttpServer server) { - server.createContext(path, PathHandler.this); + for (var path : paths) server.createContext(path, PathHandler.this); return server; } } - public Bond bindPath(String path) { + public Bond bindPath(String... path) { return new Bond(path); } - public String relativePath(HttpExchange ex) { - var path = ex.getRequestURI().toString(); - if (path.startsWith(this.path)) path = path.substring(this.path.length()); - if (path.startsWith("/")) path = path.substring(1); - return path; + public boolean doGet(String path, HttpExchange ex) throws IOException { + return false; } - - /******* begin of static methods *************/ - - public static String body(HttpExchange ex) throws IOException { - return new String(ex.getRequestBody().readAllBytes(), UTF_8); + public boolean doPost(String path, HttpExchange ex) throws IOException { + return false; } - public static Optional getAuthToken(HttpExchange ex) { - return getHeader(ex, "Authorization"); + @Override + public void handle(HttpExchange ex) throws IOException { + String path = relativePath(ex); + String method = ex.getRequestMethod(); + System.out.printf("%s %s\n", method, path); + boolean dummy = switch (method) { + case POST -> doPost(path,ex); + case GET -> doGet(path,ex); + default -> false; + }; + ex.getResponseBody().close(); } - public static Optional getHeader(HttpExchange ex, String key) { - return Optional.ofNullable(ex.getRequestHeaders().get(key)).map(List::stream).map(Stream::findFirst).orElse(Optional.empty()); - } + public String relativePath(HttpExchange ex) { + var requestPath = ex.getRequestURI().toString(); + for (var path : paths){ + if (requestPath.startsWith(path)) { + requestPath = requestPath.substring(path.length()); + break; + } + } + if (!requestPath.startsWith("/")) requestPath = "/" + requestPath; + var pos = requestPath.indexOf("?"); + if (pos >= 0) requestPath = requestPath.substring(0, pos); + return requestPath; + } - public static JSONObject json(HttpExchange ex) throws IOException { - return new JSONObject(body(ex)); - } + /******* begin of static methods *************/ - public static Optional language(HttpExchange ex) { - return getHeader(ex, "Accept-Language").map(s -> Arrays.stream(s.split(","))).map(Stream::findFirst).orElse(Optional.empty()); - } + public static String body(HttpExchange ex) throws IOException { + return new String(ex.getRequestBody().readAllBytes(), UTF_8); + } - public static void sendEmptyResponse(int statusCode, HttpExchange ex) throws IOException { - ex.sendResponseHeaders(statusCode, 0); - ex.getResponseBody().close(); + public static Optional getAuthToken(HttpExchange ex) { + return getHeader(ex, "Authorization"); + } + + public static Optional getHeader(HttpExchange ex, String key) { + return Optional.ofNullable(ex.getRequestHeaders().get(key)).map(List::stream).map(Stream::findFirst).orElse(Optional.empty()); + } + + public static boolean isPost(String method) { + return POST.equals(method); + } + + public static JSONObject json(HttpExchange ex) throws IOException { + return new JSONObject(body(ex)); + } + + public static Optional language(HttpExchange ex) { + return getHeader(ex, "Accept-Language").map(s -> Arrays.stream(s.split(","))).map(Stream::findFirst).orElse(Optional.empty()); + } + + + public static String prefix(HttpExchange ex) { + return "http://%s".formatted(ex.getRequestHeaders().getFirst("Host")); + } + + public static boolean sendEmptyResponse(int statusCode, HttpExchange ex) throws IOException { + ex.sendResponseHeaders(statusCode, 0); + return false; + } + + public static boolean sendContent(HttpExchange ex, byte[] bytes) throws IOException { + ex.sendResponseHeaders(HTTP_OK, bytes.length); + ex.getResponseBody().write(bytes); + return true; + } + + public static boolean sendContent(HttpExchange ex, Object o) throws IOException { + return sendContent(ex, o.toString().getBytes(UTF_8)); + } } -} 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 75eca43..9823706 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 @@ -18,7 +18,11 @@ 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(); @@ -36,9 +40,9 @@ public class Application { UserService userService = fileStore; SessionService sessionService = fileStore; HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); - new StaticPages(basePath).bindPath(STATIC_PATH).on(server); - new Forward(INDEX).bindPath("/").on(server); - new Backend(sessionService, userService).bindPath("/api").on(server); + new StaticPages(basePath).bindPath(STATIC_PATH, FAVICON).on(server); + new Forward(INDEX).bindPath(ROOT).on(server); + new Backend(sessionService, userService).bindPath(BACKEND, WELL_KNOWN).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 7d3afd2..690e77c 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 @@ -15,6 +15,8 @@ import java.util.Optional; import org.json.JSONObject; public class Backend extends PathHandler { + private static final String CLIENT_ID = "client_id"; + private static final String REDIRECT_URI = "redirect_uri"; private final SessionService sessions; private final UserService users; @@ -23,57 +25,81 @@ public class Backend extends PathHandler { users = userService; } - private void doLogin(HttpExchange ex) throws IOException { + private boolean authorize(HttpExchange ex, Session session) throws IOException { + var json = json(ex); + var clientId = json.getString(CLIENT_ID); + var redirect = json.getString(REDIRECT_URI); + System.out.println(json); + return sendEmptyResponse(HTTP_NOT_FOUND,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()) { - var session = sessions.createSession(user.get()); - sendUserAndCookie(ex, session); - return; - } - sendEmptyResponse(HTTP_UNAUTHORIZED, ex); + if (user.isPresent()) return sendUserAndCookie(ex, sessions.createSession(user.get())); + return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); } @Override - public void handle(HttpExchange ex) throws IOException { - String path = relativePath(ex); - String method = ex.getRequestMethod(); - System.out.printf("%s %s…", method, path); - - var session = getSession(ex); - if ("login".equals(path) && POST.equals(method)) { - doLogin(ex); // TODO: prevent brute force - return; + public boolean doGet(String path, HttpExchange ex) throws IOException { + System.out.printf("GET %s…\n", path); + switch (path) { + case "/openid-configuration": + return openidConfig(ex); } - if (session.isEmpty()) { - sendEmptyResponse(HTTP_UNAUTHORIZED, ex); - System.err.println("unauthorized"); - return; + return sendEmptyResponse(HTTP_NOT_FOUND, ex); + } + + @Override + public boolean doPost(String path, HttpExchange ex) throws IOException { + System.out.printf("POST %s…\n", path); + + // pre-login paths + switch (path) { + case "/login": + return doLogin(ex); } + var optSession = getSession(ex); + if (optSession.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); + + // post-login paths + var session = optSession.get(); switch (path) { - case "user": - sendUserAndCookie(ex, session.get()); - return; + case "/authorize": + return authorize(ex,session); + case "/user": + return sendUserAndCookie(ex, session); } System.err.println("not implemented"); - sendEmptyResponse(HTTP_NOT_FOUND, ex); + return sendEmptyResponse(HTTP_NOT_FOUND, ex); } private Optional getSession(HttpExchange ex) { return SessionToken.from(ex).map(SessionToken::sessionId).flatMap(sessions::retrieve); } - private void sendUserAndCookie(HttpExchange ex, Session session) throws IOException { + private boolean openidConfig(HttpExchange ex) throws IOException { + var uri = ex.getRequestURI().toString(); + JSONObject json = new JSONObject(); + + json.put("authorization_endpoint", prefix(ex) + "/web/authorization.html"); + return sendContent(ex, json); + } + + + private boolean 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(session.id()).addTo(headers); ex.sendResponseHeaders(200, bytes.length); - ex.getResponseBody().write(bytes); + var out = ex.getResponseBody(); + out.write(bytes); + return true; } } diff --git a/de.srsoftware.oidc.web/src/main/java/de/srsoftware/oidc/web/Forward.java b/de.srsoftware.oidc.web/src/main/java/de/srsoftware/oidc/web/Forward.java index aa915ab..f9136e3 100644 --- a/de.srsoftware.oidc.web/src/main/java/de/srsoftware/oidc/web/Forward.java +++ b/de.srsoftware.oidc.web/src/main/java/de/srsoftware/oidc/web/Forward.java @@ -14,10 +14,10 @@ public class Forward extends PathHandler { } @Override - public void handle(HttpExchange ex) throws IOException { - System.out.printf("Forwarding (%d) %s to %s…\n", CODE, ex.getRequestURI(), toPath); + public boolean doGet(String path, HttpExchange ex) throws IOException { + System.out.printf("Forwarding (%d) %s to %s…\n", CODE, path, toPath); ex.getResponseHeaders().add("Location", toPath); ex.sendResponseHeaders(CODE, 0); - ex.getResponseBody().close(); + return true; } } diff --git a/de.srsoftware.oidc.web/src/main/java/de/srsoftware/oidc/web/StaticPages.java b/de.srsoftware.oidc.web/src/main/java/de/srsoftware/oidc/web/StaticPages.java index 84e14ed..ea0c9c0 100644 --- a/de.srsoftware.oidc.web/src/main/java/de/srsoftware/oidc/web/StaticPages.java +++ b/de.srsoftware.oidc.web/src/main/java/de/srsoftware/oidc/web/StaticPages.java @@ -1,11 +1,12 @@ /* © SRSoftware 2024 */ package de.srsoftware.oidc.web; +import static java.net.HttpURLConnection.HTTP_NOT_FOUND; + import com.sun.net.httpserver.HttpExchange; import de.srsoftware.oidc.api.PathHandler; import java.io.FileNotFoundException; import java.io.IOException; -import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URL; import java.nio.file.Files; @@ -14,6 +15,7 @@ import java.util.Optional; public class StaticPages extends PathHandler { private static final String DEFAULT_LANGUAGE = "en"; + private static final String FAVICON = "favicon.ico"; private final Optional base; private ClassLoader loader; @@ -27,27 +29,22 @@ public class StaticPages extends PathHandler { private static final String INDEX = "en/index.html"; @Override - public void handle(HttpExchange ex) throws IOException { - String relativePath = relativePath(ex); - String lang = language(ex).orElse(DEFAULT_LANGUAGE); - String method = ex.getRequestMethod(); - - if (relativePath.isBlank()) relativePath = INDEX; - System.out.printf("%s %s: ", method, ex.getRequestURI()); + public boolean doGet(String relativePath, HttpExchange ex) throws IOException { + String lang = language(ex).orElse(DEFAULT_LANGUAGE); + if (relativePath.startsWith("/")) relativePath = relativePath.substring(1); + if (relativePath.isBlank()) { + relativePath = ex.getRequestURI().toString().endsWith(FAVICON) ? FAVICON : INDEX; + } try { - System.out.printf("Loading %s for lagnuage %s…", relativePath, lang); + System.out.printf("Loading %s for language %s…", relativePath, lang); Response response = loadFile(lang, relativePath).orElseThrow(() -> new FileNotFoundException()); ex.getResponseHeaders().add(CONTENT_TYPE, response.contentType); - ex.sendResponseHeaders(200, response.content.length); - OutputStream os = ex.getResponseBody(); - os.write(response.content); - os.close(); System.out.println("success."); + return sendContent(ex, response.content); } catch (FileNotFoundException fnf) { - ex.sendResponseHeaders(404, 0); - ex.getResponseBody().close(); System.err.println("failed!"); + return sendEmptyResponse(HTTP_NOT_FOUND, ex); } } diff --git a/de.srsoftware.oidc.web/src/main/resources/en/authorization.html b/de.srsoftware.oidc.web/src/main/resources/en/authorization.html new file mode 100644 index 0000000..e6f76e7 --- /dev/null +++ b/de.srsoftware.oidc.web/src/main/resources/en/authorization.html @@ -0,0 +1,13 @@ + + + + Light OIDC + + + + + +

Authorization!

+Not implemented, yet! + + \ No newline at end of file diff --git a/de.srsoftware.oidc.web/src/main/resources/en/authorization.js b/de.srsoftware.oidc.web/src/main/resources/en/authorization.js new file mode 100644 index 0000000..d8d1816 --- /dev/null +++ b/de.srsoftware.oidc.web/src/main/resources/en/authorization.js @@ -0,0 +1,11 @@ +var params = new URLSearchParams(window.location.search) +var json = Object.fromEntries(params); + +fetch(api+"/authorize",{ + method: 'POST', + body: JSON.stringify(json), + headers: { + 'Content-Type': 'application/json' + } +}) + diff --git a/de.srsoftware.oidc.web/src/main/resources/en/favicon.ico b/de.srsoftware.oidc.web/src/main/resources/en/favicon.ico new file mode 100644 index 0000000..bc64605 Binary files /dev/null and b/de.srsoftware.oidc.web/src/main/resources/en/favicon.ico differ diff --git a/de.srsoftware.oidc.web/src/main/resources/en/index.html b/de.srsoftware.oidc.web/src/main/resources/en/index.html index 16069ad..d96e342 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/index.html +++ b/de.srsoftware.oidc.web/src/main/resources/en/index.html @@ -1,10 +1,24 @@ + Light OIDC - +

Welcome!

+

Connected sites

+ These are sites that are connected with your account: + + + + + + + + +
SiteActions
+ +
\ No newline at end of file diff --git a/de.srsoftware.oidc.web/src/main/resources/en/index.js b/de.srsoftware.oidc.web/src/main/resources/en/index.js deleted file mode 100644 index 5b558c9..0000000 --- a/de.srsoftware.oidc.web/src/main/resources/en/index.js +++ /dev/null @@ -1,7 +0,0 @@ -const UNAUTHORIZED = 401; - -function handleUser(response){ - console.log(response); -} - -fetch(api+"/user").then(handleUser); \ No newline at end of file diff --git a/de.srsoftware.oidc.web/src/main/resources/en/login.html b/de.srsoftware.oidc.web/src/main/resources/en/login.html index 81a4195..b50619f 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/login.html +++ b/de.srsoftware.oidc.web/src/main/resources/en/login.html @@ -1,25 +1,24 @@ + Light OIDC - +

Login

-
-
- User credentials +
+ User credentials -
- +
\ No newline at end of file diff --git a/de.srsoftware.oidc.web/src/main/resources/en/login.js b/de.srsoftware.oidc.web/src/main/resources/en/login.js new file mode 100644 index 0000000..48f35e5 --- /dev/null +++ b/de.srsoftware.oidc.web/src/main/resources/en/login.js @@ -0,0 +1,33 @@ +async function handleLogin(response){ + if (response.ok){ + var body = await response.json(); + + setTimeout(doRedirect,100); + } + return false; +} + +function doRedirect(){ + let params = new URL(document.location.toString()).searchParams; + let redirect = params.get("return_to") || 'index.html'; + window.location.href = redirect,true; + return false; +} + +function tryLogin(){ + document.getElementById("error").innerHTML = ""; + var username = document.getElementById('username').value; + var password = document.getElementById('password').value; + fetch(api+"/login",{ + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + username : username, + password : password + }) + }).then(handleLogin); + return false; +} \ No newline at end of file diff --git a/de.srsoftware.oidc.web/src/main/resources/en/newclient.html b/de.srsoftware.oidc.web/src/main/resources/en/newclient.html new file mode 100644 index 0000000..0e5f512 --- /dev/null +++ b/de.srsoftware.oidc.web/src/main/resources/en/newclient.html @@ -0,0 +1,28 @@ + + + + Light OIDC + + + + +

Add new client

+
+ Settings + + + + + + + + + + + + + +
client name
client secret
redirect urls
+
+ + \ No newline at end of file diff --git a/de.srsoftware.oidc.web/src/main/resources/en/user.js b/de.srsoftware.oidc.web/src/main/resources/en/user.js new file mode 100644 index 0000000..c92a9ef --- /dev/null +++ b/de.srsoftware.oidc.web/src/main/resources/en/user.js @@ -0,0 +1,12 @@ +const UNAUTHORIZED = 401; + +async function handleUser(response){ + if (response.status == UNAUTHORIZED) { + window.location.href = 'login.html?return_to='+encodeURI(window.location.href); + return; + } + var user = await response.json(); + // TODO: load navigation +} + +fetch(api+"/user",{method:'POST'}).then(handleUser); \ No newline at end of file