Browse Source

improving path handling, working on authorization flow

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
sqlite
Stephan Richter 4 months ago
parent
commit
993c59bfa6
  1. 72
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/PathHandler.java
  2. 10
      de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java
  3. 78
      de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/Backend.java
  4. 6
      de.srsoftware.oidc.web/src/main/java/de/srsoftware/oidc/web/Forward.java
  5. 25
      de.srsoftware.oidc.web/src/main/java/de/srsoftware/oidc/web/StaticPages.java
  6. 13
      de.srsoftware.oidc.web/src/main/resources/en/authorization.html
  7. 11
      de.srsoftware.oidc.web/src/main/resources/en/authorization.js
  8. BIN
      de.srsoftware.oidc.web/src/main/resources/en/favicon.ico
  9. 16
      de.srsoftware.oidc.web/src/main/resources/en/index.html
  10. 7
      de.srsoftware.oidc.web/src/main/resources/en/index.js
  11. 11
      de.srsoftware.oidc.web/src/main/resources/en/login.html
  12. 33
      de.srsoftware.oidc.web/src/main/resources/en/login.js
  13. 28
      de.srsoftware.oidc.web/src/main/resources/en/newclient.html
  14. 12
      de.srsoftware.oidc.web/src/main/resources/en/user.js

72
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/PathHandler.java

@ -1,6 +1,7 @@
/* © SRSoftware 2024 */ /* © SRSoftware 2024 */
package de.srsoftware.oidc.api; package de.srsoftware.oidc.api;
import static java.net.HttpURLConnection.HTTP_OK;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
@ -15,30 +16,58 @@ import org.json.JSONObject;
public abstract class PathHandler implements HttpHandler { public abstract class PathHandler implements HttpHandler {
public static final String CONTENT_TYPE = "Content-Type"; 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 JSON = "application/json";
public static final String POST = "POST"; public static final String POST = "POST";
private String path; private String[] paths;
public class Bond { public class Bond {
Bond(String p) { Bond(String[] paths) {
path = p; PathHandler.this.paths = paths;
} }
public HttpServer on(HttpServer server) { public HttpServer on(HttpServer server) {
server.createContext(path, PathHandler.this); for (var path : paths) server.createContext(path, PathHandler.this);
return server; return server;
} }
} }
public Bond bindPath(String path) { public Bond bindPath(String... path) {
return new Bond(path); return new Bond(path);
} }
public boolean doGet(String path, HttpExchange ex) throws IOException {
return false;
}
public boolean doPost(String path, HttpExchange ex) throws IOException {
return false;
}
@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 String relativePath(HttpExchange ex) { public String relativePath(HttpExchange ex) {
var path = ex.getRequestURI().toString(); var requestPath = ex.getRequestURI().toString();
if (path.startsWith(this.path)) path = path.substring(this.path.length()); for (var path : paths){
if (path.startsWith("/")) path = path.substring(1); if (requestPath.startsWith(path)) {
return 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;
} }
/******* begin of static methods *************/ /******* begin of static methods *************/
@ -55,6 +84,10 @@ public abstract class PathHandler implements HttpHandler {
return Optional.ofNullable(ex.getRequestHeaders().get(key)).map(List::stream).map(Stream::findFirst).orElse(Optional.empty()); 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 { public static JSONObject json(HttpExchange ex) throws IOException {
return new JSONObject(body(ex)); return new JSONObject(body(ex));
} }
@ -63,8 +96,23 @@ public abstract class PathHandler implements HttpHandler {
return getHeader(ex, "Accept-Language").map(s -> Arrays.stream(s.split(","))).map(Stream::findFirst).orElse(Optional.empty()); return getHeader(ex, "Accept-Language").map(s -> Arrays.stream(s.split(","))).map(Stream::findFirst).orElse(Optional.empty());
} }
public static void sendEmptyResponse(int statusCode, HttpExchange ex) throws IOException {
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); ex.sendResponseHeaders(statusCode, 0);
ex.getResponseBody().close(); 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));
}
} }
}

10
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; import java.util.concurrent.Executors;
public class Application { 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"; 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 = "admin";
public static final String FIRST_USER_PASS = "admin"; public static final String FIRST_USER_PASS = "admin";
public static final String FIRST_UUID = UUID.randomUUID().toString(); public static final String FIRST_UUID = UUID.randomUUID().toString();
@ -36,9 +40,9 @@ public class Application {
UserService userService = fileStore; UserService userService = fileStore;
SessionService sessionService = fileStore; SessionService sessionService = fileStore;
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
new StaticPages(basePath).bindPath(STATIC_PATH).on(server); new StaticPages(basePath).bindPath(STATIC_PATH, FAVICON).on(server);
new Forward(INDEX).bindPath("/").on(server); new Forward(INDEX).bindPath(ROOT).on(server);
new Backend(sessionService, userService).bindPath("/api").on(server); new Backend(sessionService, userService).bindPath(BACKEND, WELL_KNOWN).on(server);
server.setExecutor(Executors.newCachedThreadPool()); server.setExecutor(Executors.newCachedThreadPool());
server.start(); server.start();
} }

78
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; import org.json.JSONObject;
public class Backend extends PathHandler { 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 SessionService sessions;
private final UserService users; private final UserService users;
@ -23,57 +25,81 @@ public class Backend extends PathHandler {
users = userService; 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 body = json(ex);
var username = body.has(USERNAME) ? body.getString(USERNAME) : null; var username = body.has(USERNAME) ? body.getString(USERNAME) : null;
var password = body.has(PASSWORD) ? body.getString(PASSWORD) : null; var password = body.has(PASSWORD) ? body.getString(PASSWORD) : null;
Optional<User> user = users.load(username, password); Optional<User> user = users.load(username, password);
if (user.isPresent()) { if (user.isPresent()) return sendUserAndCookie(ex, sessions.createSession(user.get()));
var session = sessions.createSession(user.get()); return sendEmptyResponse(HTTP_UNAUTHORIZED, ex);
sendUserAndCookie(ex, session);
return;
}
sendEmptyResponse(HTTP_UNAUTHORIZED, ex);
} }
@Override @Override
public void handle(HttpExchange ex) throws IOException { public boolean doGet(String path, HttpExchange ex) throws IOException {
String path = relativePath(ex); System.out.printf("GET %s…\n", path);
String method = ex.getRequestMethod(); switch (path) {
System.out.printf("%s %s…", method, path); case "/openid-configuration":
return openidConfig(ex);
var session = getSession(ex);
if ("login".equals(path) && POST.equals(method)) {
doLogin(ex); // TODO: prevent brute force
return;
} }
if (session.isEmpty()) { return sendEmptyResponse(HTTP_NOT_FOUND, ex);
sendEmptyResponse(HTTP_UNAUTHORIZED, ex); }
System.err.println("unauthorized");
return; @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) { switch (path) {
case "user": case "/authorize":
sendUserAndCookie(ex, session.get()); return authorize(ex,session);
return; case "/user":
return sendUserAndCookie(ex, session);
} }
System.err.println("not implemented"); System.err.println("not implemented");
sendEmptyResponse(HTTP_NOT_FOUND, ex); return sendEmptyResponse(HTTP_NOT_FOUND, ex);
} }
private Optional<Session> getSession(HttpExchange ex) { private Optional<Session> getSession(HttpExchange ex) {
return SessionToken.from(ex).map(SessionToken::sessionId).flatMap(sessions::retrieve); 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 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(session.id()).addTo(headers); new SessionToken(session.id()).addTo(headers);
ex.sendResponseHeaders(200, bytes.length); ex.sendResponseHeaders(200, bytes.length);
ex.getResponseBody().write(bytes); var out = ex.getResponseBody();
out.write(bytes);
return true;
} }
} }

6
de.srsoftware.oidc.web/src/main/java/de/srsoftware/oidc/web/Forward.java

@ -14,10 +14,10 @@ public class Forward extends PathHandler {
} }
@Override @Override
public void handle(HttpExchange ex) throws IOException { public boolean doGet(String path, HttpExchange ex) throws IOException {
System.out.printf("Forwarding (%d) %s to %s…\n", CODE, ex.getRequestURI(), toPath); System.out.printf("Forwarding (%d) %s to %s…\n", CODE, path, toPath);
ex.getResponseHeaders().add("Location", toPath); ex.getResponseHeaders().add("Location", toPath);
ex.sendResponseHeaders(CODE, 0); ex.sendResponseHeaders(CODE, 0);
ex.getResponseBody().close(); return true;
} }
} }

25
de.srsoftware.oidc.web/src/main/java/de/srsoftware/oidc/web/StaticPages.java

@ -1,11 +1,12 @@
/* © SRSoftware 2024 */ /* © SRSoftware 2024 */
package de.srsoftware.oidc.web; package de.srsoftware.oidc.web;
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.oidc.api.PathHandler; import de.srsoftware.oidc.api.PathHandler;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.nio.file.Files; import java.nio.file.Files;
@ -14,6 +15,7 @@ import java.util.Optional;
public class StaticPages extends PathHandler { public class StaticPages extends PathHandler {
private static final String DEFAULT_LANGUAGE = "en"; private static final String DEFAULT_LANGUAGE = "en";
private static final String FAVICON = "favicon.ico";
private final Optional<Path> base; private final Optional<Path> base;
private ClassLoader loader; private ClassLoader loader;
@ -27,27 +29,22 @@ public class StaticPages extends PathHandler {
private static final String INDEX = "en/index.html"; private static final String INDEX = "en/index.html";
@Override @Override
public void handle(HttpExchange ex) throws IOException { public boolean doGet(String relativePath, HttpExchange ex) throws IOException {
String relativePath = relativePath(ex);
String lang = language(ex).orElse(DEFAULT_LANGUAGE); String lang = language(ex).orElse(DEFAULT_LANGUAGE);
String method = ex.getRequestMethod(); if (relativePath.startsWith("/")) relativePath = relativePath.substring(1);
if (relativePath.isBlank()) {
if (relativePath.isBlank()) relativePath = INDEX; relativePath = ex.getRequestURI().toString().endsWith(FAVICON) ? FAVICON : INDEX;
System.out.printf("%s %s: ", method, ex.getRequestURI()); }
try { 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()); Response response = loadFile(lang, relativePath).orElseThrow(() -> new FileNotFoundException());
ex.getResponseHeaders().add(CONTENT_TYPE, response.contentType); 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."); System.out.println("success.");
return sendContent(ex, response.content);
} catch (FileNotFoundException fnf) { } catch (FileNotFoundException fnf) {
ex.sendResponseHeaders(404, 0);
ex.getResponseBody().close();
System.err.println("failed!"); System.err.println("failed!");
return sendEmptyResponse(HTTP_NOT_FOUND, ex);
} }
} }

13
de.srsoftware.oidc.web/src/main/resources/en/authorization.html

@ -0,0 +1,13 @@
<html>
<head>
<meta charset="utf-8">
<title>Light OIDC</title>
<script src="config.js"></script>
<script src="user.js"></script>
<script src="authorization.js"></script>
</head>
<body>
<h1>Authorization!</h1>
Not implemented, yet!
</body>
</html>

11
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'
}
})

BIN
de.srsoftware.oidc.web/src/main/resources/en/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

16
de.srsoftware.oidc.web/src/main/resources/en/index.html

@ -1,10 +1,24 @@
<html> <html>
<head> <head>
<meta charset="utf-8">
<title>Light OIDC</title> <title>Light OIDC</title>
<script src="config.js"></script> <script src="config.js"></script>
<script src="index.js"></script> <script src="user.js"></script>
</head> </head>
<body> <body>
<h1>Welcome!</h1> <h1>Welcome!</h1>
<h2>Connected sites</h2>
These are sites that are connected with your account:
<table>
<tr>
<th>Site</th>
<th>Actions</th>
</tr>
<tr>
<td>
<button onclick="window.location.href='newclient.html';">Add new site…</button>
</td>
</tr>
</table>
</body> </body>
</html> </html>

7
de.srsoftware.oidc.web/src/main/resources/en/index.js

@ -1,7 +0,0 @@
const UNAUTHORIZED = 401;
function handleUser(response){
console.log(response);
}
fetch(api+"/user").then(handleUser);

11
de.srsoftware.oidc.web/src/main/resources/en/login.html

@ -1,25 +1,24 @@
<html> <html>
<head> <head>
<meta charset="utf-8">
<title>Light OIDC</title> <title>Light OIDC</title>
<script src="config.js"></script> <script src="config.js"></script>
<script src="index.js"></script> <script src="login.js"></script>
</head> </head>
<body> <body>
<h1>Login</h1> <h1>Login</h1>
<form id="login"> <fieldset id="login">
<fieldset>
<legend>User credentials</legend> <legend>User credentials</legend>
<label> <label>
Username Username
<input type="text" name="username" /> <input type="text" id="username" />
</label> </label>
<label> <label>
Password Password
<input type="password" name="password" /> <input type="password" id="password" />
</label> </label>
<button type="button" onClick="tryLogin()">Login</button> <button type="button" onClick="tryLogin()">Login</button>
</fieldset> </fieldset>
</form>
<div id="error"></div> <div id="error"></div>
</body> </body>
</html> </html>

33
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;
}

28
de.srsoftware.oidc.web/src/main/resources/en/newclient.html

@ -0,0 +1,28 @@
<html>
<head>
<meta charset="utf-8">
<title>Light OIDC</title>
<script src="config.js"></script>
<script src="user.js"></script>
</head>
<body>
<h1>Add new client</h1>
<fieldset>
<legend>Settings</legend>
<table>
<tr>
<th>client name</th>
<td><input type="text" size="50" id="client-name"></td>
</tr>
<tr>
<th>client secret</th>
<td><input type="text" size="50" id="client-secret"></td>
</tr>
<tr>
<th>redirect urls</th>
<td><textarea cols="50" rows="5" id="redirect-urls"></textarea></td>
</tr>
</table>
</fieldset>
</body>
</html>

12
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);
Loading…
Cancel
Save