diff --git a/de.srsoftware.http/src/main/java/de/srsoftware/http/PathHandler.java b/de.srsoftware.http/src/main/java/de/srsoftware/http/PathHandler.java index 20ffb4d..dc7e381 100644 --- a/de.srsoftware.http/src/main/java/de/srsoftware/http/PathHandler.java +++ b/de.srsoftware.http/src/main/java/de/srsoftware/http/PathHandler.java @@ -10,6 +10,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpsExchange; import java.io.IOException; import java.util.*; import java.util.stream.Collectors; @@ -127,7 +128,8 @@ public abstract class PathHandler implements HttpHandler { var headers = ex.getRequestHeaders(); var host = headers.getFirst(FORWARDED_HOST); if (host == null) host = headers.getFirst(HOST); - return host == null ? null : "https://" + host; + var proto = nullable(headers.getFirst("X-forwarded-proto")).orElseGet(() -> ex instanceof HttpsExchange ? "https" : "http"); + return host == null ? null : proto + "://" + host; } public static JSONObject json(HttpExchange ex) throws IOException { diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionService.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionService.java index f1b8332..abbecf5 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionService.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionService.java @@ -9,7 +9,7 @@ import java.util.Optional; public interface SessionService { Session createSession(User user); SessionService dropSession(String sessionId); - Session extend(String sessionId); + Session extend(Session session); Optional retrieve(String sessionId); SessionService setDuration(Duration duration); } 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 index b14083f..a8d2b02 100644 --- 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 @@ -110,6 +110,7 @@ public class ClientController extends Controller { // post-login paths var session = optSession.get(); + sessions.extend(session); switch (path) { case "/": return deleteClient(ex, session); @@ -125,6 +126,7 @@ public class ClientController extends Controller { // post-login paths var session = optSession.get(); + sessions.extend(session); switch (path) { case "/": return load(ex, session); diff --git a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/EmailController.java b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/EmailController.java index 607985e..cad4fe9 100644 --- a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/EmailController.java +++ b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/EmailController.java @@ -9,7 +9,7 @@ import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; import com.sun.net.httpserver.HttpExchange; import de.srsoftware.oidc.api.MailConfig; import de.srsoftware.oidc.api.SessionService; -import de.srsoftware.oidc.api.data.User; +import de.srsoftware.oidc.api.data.Session; import java.io.IOException; public class EmailController extends Controller { @@ -24,10 +24,11 @@ public class EmailController extends Controller { public boolean doGet(String path, HttpExchange ex) throws IOException { var optSession = getSession(ex); if (optSession.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); - var user = optSession.get().user(); + var session = optSession.get(); + sessions.extend(session); switch (path) { case "/settings": - return provideSettings(ex, user); + return provideSettings(ex, session); } return notFound(ex); } @@ -36,21 +37,23 @@ public class EmailController extends Controller { public boolean doPost(String path, HttpExchange ex) throws IOException { var optSession = getSession(ex); if (optSession.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); - var user = optSession.get().user(); + var session = optSession.get(); + sessions.extend(session); + switch (path) { case "/settings": - return saveSettings(ex, user); + return saveSettings(ex, session); } return notFound(ex); } - private boolean provideSettings(HttpExchange ex, User user) throws IOException { - if (!user.hasPermission(MANAGE_SMTP)) return sendEmptyResponse(HTTP_FORBIDDEN, ex); + private boolean provideSettings(HttpExchange ex, Session session) throws IOException { + if (!session.user().hasPermission(MANAGE_SMTP)) return sendEmptyResponse(HTTP_FORBIDDEN, ex); return sendContent(ex, mailConfig.map()); } - private boolean saveSettings(HttpExchange ex, User user) throws IOException { - if (!user.hasPermission(MANAGE_SMTP)) return sendEmptyResponse(HTTP_FORBIDDEN, ex); + private boolean saveSettings(HttpExchange ex, Session session) throws IOException { + if (!session.user().hasPermission(MANAGE_SMTP)) return sendEmptyResponse(HTTP_FORBIDDEN, ex); var data = json(ex); if (data.has(SMTP_HOST)) mailConfig.smtpHost(data.getString(SMTP_HOST)); if (data.has(SMTP_PORT)) mailConfig.smtpPort(data.getInt(SMTP_PORT)); 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 index 07b7eb7..31361b6 100644 --- 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 @@ -51,6 +51,8 @@ public class UserController extends Controller { // post-login paths var session = optSession.get(); + sessions.extend(session); + switch (path) { case "/delete": return deleteUser(ex, session); @@ -84,6 +86,8 @@ public class UserController extends Controller { // post-login paths var session = optSession.get(); + sessions.extend(session); + switch (path) { case "/logout": return logout(ex, session); @@ -106,6 +110,8 @@ public class UserController extends Controller { // post-login paths var session = optSession.get(); + sessions.extend(session); + switch (path) { case "/": return sendUserAndCookie(ex, session); @@ -121,28 +127,26 @@ public class UserController extends Controller { return notFound(ex); } - private boolean resetPassword(HttpExchange ex) throws IOException { - var data = json(ex); - if (!data.has(TOKEN)) return sendContent(ex, HTTP_UNAUTHORIZED, "token missing"); - var newpass = data.getJSONArray("newpass"); - var newPass1 = newpass.getString(0); - if (!newPass1.equals(newpass.getString(1))) { - return badRequest(ex, "password mismatch"); + + private boolean generateResetLink(HttpExchange ex) throws IOException { + var idOrEmail = queryParam(ex).get("user"); + var url = url(ex) // + .replace("/api/user/", "/web/") + .split("\\?")[0] + + ".html"; + Set matchingUsers = users.find(idOrEmail); + if (!matchingUsers.isEmpty()) { + resourceLoader // + .loadFile(language(ex), "reset_password.template.txt") + .map(ResourceLoader.Resource::content) + .map(bytes -> new String(bytes, UTF_8)) + .ifPresent(template -> { // + matchingUsers.forEach(user -> sendResetLink(user, template, url)); + }); } - if (!strong(newPass1)) return sendContent(ex, HTTP_BAD_REQUEST, "weak password"); - var token = data.getString(TOKEN); - var optUser = users.consumeToken(token); - if (optUser.isEmpty()) return sendContent(ex, HTTP_UNAUTHORIZED, "invalid token"); - var user = optUser.get(); - users.updatePassword(user, newPass1); - var session = sessions.createSession(user); - new SessionToken(session.id()).addTo(ex); - return sendRedirect(ex, "/"); + return sendEmptyResponse(HTTP_OK, ex); } - private boolean strong(String pass) { - return pass.length() > 10; // TODO - } private boolean list(HttpExchange ex, Session session) throws IOException { var user = session.user(); @@ -169,25 +173,31 @@ public class UserController extends Controller { return sendEmptyResponse(HTTP_OK, ex); } - private boolean generateResetLink(HttpExchange ex) throws IOException { - var idOrEmail = queryParam(ex).get("user"); - var url = url(ex) // - .replace("/api/user/", "/web/") - .split("\\?")[0] + - ".html"; - Set matchingUsers = users.find(idOrEmail); - if (!matchingUsers.isEmpty()) { - resourceLoader // - .loadFile(language(ex), "reset_password.template.txt") - .map(ResourceLoader.Resource::content) - .map(bytes -> new String(bytes, UTF_8)) - .ifPresent(template -> { // - matchingUsers.forEach(user -> sendResetLink(user, template, url)); - }); + + private boolean resetPassword(HttpExchange ex) throws IOException { + var data = json(ex); + if (!data.has(TOKEN)) return sendContent(ex, HTTP_UNAUTHORIZED, "token missing"); + var passwords = data.getJSONArray("newpass"); + var newPass = passwords.getString(0); + if (!newPass.equals(passwords.getString(1))) { + return badRequest(ex, "password mismatch"); } - return sendEmptyResponse(HTTP_OK, ex); + try { + strong(newPass); + } catch (RuntimeException e) { + return sendContent(ex, HTTP_BAD_REQUEST, e.getMessage()); + } + var token = data.getString(TOKEN); + var optUser = users.consumeToken(token); + if (optUser.isEmpty()) return sendContent(ex, HTTP_UNAUTHORIZED, "invalid token"); + var user = optUser.get(); + users.updatePassword(user, newPass); + var session = sessions.createSession(user); + new SessionToken(session.id()).addTo(ex); + return sendRedirect(ex, "/"); } + private void sendResetLink(User user, String template, String url) { LOG.log(WARNING, "Sending password link to {0}", user.email()); var token = users.accessToken(user); @@ -228,6 +238,30 @@ public class UserController extends Controller { return sendContent(ex, session.user().map(false)); } + + private void strong(String pass) { + var digits = false; + var special = false; + var alpha = false; + for (int i = 0; i < pass.length(); i++) { + char c = pass.charAt(i); + if (Character.isDigit(c)) { + digits = true; + } else if (Character.isAlphabetic(c)) { + alpha = true; + } else + special = true; + } + if (pass.length() < 16) { + if (!alpha) throw new RuntimeException("Passwords shorter than 16 characters must contain alphabetic characters!"); + if (!digits && !special) throw new RuntimeException("Passwords shorter than 16 characters must contain at least one digit or special character!"); + if (pass.length() < 10) { + if (!digits || !special) throw new RuntimeException("Passwords shorter than 10 characters must contain digits as well as alphabetic and special characters!"); + if (pass.length() < 6) throw new RuntimeException("Password must have at least 6 characters!"); + } + } + } + private boolean updatePassword(HttpExchange ex, Session session) throws IOException { var user = session.user(); var json = json(ex); @@ -238,12 +272,17 @@ public class UserController extends Controller { 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))) { + var passwords = json.getJSONArray("newpass"); + var newPass = passwords.getString(0); + if (!newPass.equals(passwords.getString(1))) { return badRequest(ex, "password mismatch"); } - users.updatePassword(user, newPass1); + try { + strong(newPass); + } catch (RuntimeException e) { + return sendContent(ex, HTTP_BAD_REQUEST, e.getMessage()); + } + users.updatePassword(user, newPass); return sendContent(ex, user.map(false)); } diff --git a/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java b/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java index f97059e..89808c9 100644 --- a/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java +++ b/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java @@ -239,8 +239,10 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe } @Override - public Session extend(String sessionId) { - return null; + public Session extend(Session session) { + var user = session.user(); + var endOfSession = Instant.now().plus(user.sessionDuration()); + return save(new Session(user, endOfSession, session.id())); } private JSONObject sessions() { diff --git a/de.srsoftware.oidc.web/src/main/resources/en/todo.html b/de.srsoftware.oidc.web/src/main/resources/en/todo.html index 7b0f1cb..cc330e6 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/todo.html +++ b/de.srsoftware.oidc.web/src/main/resources/en/todo.html @@ -12,12 +12,8 @@

to do…

    -
  • at_hash in ID Token
  • -
  • Session bei Aktivität verlängern
  • implement token refresh
  • -
  • handle https correctly in PathHandler.hostname
  • Verschlüsselung im config-File
  • -
  • bessere Implementierung für UserController.strong(pass), anwendung überall da wo passworte geändert werden können