Browse Source

implemented:

- at_hash in id-token
- testing for strong passwords
- better protocol detection in PathHandler
- session extension on user activity

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
sqlite
Stephan Richter 3 months ago
parent
commit
d6007493df
  1. 4
      de.srsoftware.http/src/main/java/de/srsoftware/http/PathHandler.java
  2. 2
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionService.java
  3. 2
      de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/ClientController.java
  4. 21
      de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/EmailController.java
  5. 117
      de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java
  6. 6
      de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java
  7. 4
      de.srsoftware.oidc.web/src/main/resources/en/todo.html

4
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.HttpExchange;
import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpsExchange;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -127,7 +128,8 @@ public abstract class PathHandler implements HttpHandler {
var headers = ex.getRequestHeaders(); var headers = ex.getRequestHeaders();
var host = headers.getFirst(FORWARDED_HOST); var host = headers.getFirst(FORWARDED_HOST);
if (host == null) host = headers.getFirst(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 { public static JSONObject json(HttpExchange ex) throws IOException {

2
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/SessionService.java

@ -9,7 +9,7 @@ import java.util.Optional;
public interface SessionService { public interface SessionService {
Session createSession(User user); Session createSession(User user);
SessionService dropSession(String sessionId); SessionService dropSession(String sessionId);
Session extend(String sessionId); Session extend(Session session);
Optional<Session> retrieve(String sessionId); Optional<Session> retrieve(String sessionId);
SessionService setDuration(Duration duration); SessionService setDuration(Duration duration);
} }

2
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 // post-login paths
var session = optSession.get(); var session = optSession.get();
sessions.extend(session);
switch (path) { switch (path) {
case "/": case "/":
return deleteClient(ex, session); return deleteClient(ex, session);
@ -125,6 +126,7 @@ public class ClientController extends Controller {
// post-login paths // post-login paths
var session = optSession.get(); var session = optSession.get();
sessions.extend(session);
switch (path) { switch (path) {
case "/": case "/":
return load(ex, session); return load(ex, session);

21
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 com.sun.net.httpserver.HttpExchange;
import de.srsoftware.oidc.api.MailConfig; import de.srsoftware.oidc.api.MailConfig;
import de.srsoftware.oidc.api.SessionService; import de.srsoftware.oidc.api.SessionService;
import de.srsoftware.oidc.api.data.User; import de.srsoftware.oidc.api.data.Session;
import java.io.IOException; import java.io.IOException;
public class EmailController extends Controller { public class EmailController extends Controller {
@ -24,10 +24,11 @@ public class EmailController extends Controller {
public boolean doGet(String path, HttpExchange ex) throws IOException { public boolean doGet(String path, HttpExchange ex) throws IOException {
var optSession = getSession(ex); var optSession = getSession(ex);
if (optSession.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); if (optSession.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex);
var user = optSession.get().user(); var session = optSession.get();
sessions.extend(session);
switch (path) { switch (path) {
case "/settings": case "/settings":
return provideSettings(ex, user); return provideSettings(ex, session);
} }
return notFound(ex); return notFound(ex);
} }
@ -36,21 +37,23 @@ public class EmailController extends Controller {
public boolean doPost(String path, HttpExchange ex) throws IOException { public boolean doPost(String path, HttpExchange ex) throws IOException {
var optSession = getSession(ex); var optSession = getSession(ex);
if (optSession.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); if (optSession.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex);
var user = optSession.get().user(); var session = optSession.get();
sessions.extend(session);
switch (path) { switch (path) {
case "/settings": case "/settings":
return saveSettings(ex, user); return saveSettings(ex, session);
} }
return notFound(ex); return notFound(ex);
} }
private boolean provideSettings(HttpExchange ex, User user) throws IOException { private boolean provideSettings(HttpExchange ex, Session session) throws IOException {
if (!user.hasPermission(MANAGE_SMTP)) return sendEmptyResponse(HTTP_FORBIDDEN, ex); if (!session.user().hasPermission(MANAGE_SMTP)) return sendEmptyResponse(HTTP_FORBIDDEN, ex);
return sendContent(ex, mailConfig.map()); return sendContent(ex, mailConfig.map());
} }
private boolean saveSettings(HttpExchange ex, User user) throws IOException { private boolean saveSettings(HttpExchange ex, Session session) throws IOException {
if (!user.hasPermission(MANAGE_SMTP)) return sendEmptyResponse(HTTP_FORBIDDEN, ex); if (!session.user().hasPermission(MANAGE_SMTP)) return sendEmptyResponse(HTTP_FORBIDDEN, ex);
var data = json(ex); var data = json(ex);
if (data.has(SMTP_HOST)) mailConfig.smtpHost(data.getString(SMTP_HOST)); if (data.has(SMTP_HOST)) mailConfig.smtpHost(data.getString(SMTP_HOST));
if (data.has(SMTP_PORT)) mailConfig.smtpPort(data.getInt(SMTP_PORT)); if (data.has(SMTP_PORT)) mailConfig.smtpPort(data.getInt(SMTP_PORT));

117
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 // post-login paths
var session = optSession.get(); var session = optSession.get();
sessions.extend(session);
switch (path) { switch (path) {
case "/delete": case "/delete":
return deleteUser(ex, session); return deleteUser(ex, session);
@ -84,6 +86,8 @@ public class UserController extends Controller {
// post-login paths // post-login paths
var session = optSession.get(); var session = optSession.get();
sessions.extend(session);
switch (path) { switch (path) {
case "/logout": case "/logout":
return logout(ex, session); return logout(ex, session);
@ -106,6 +110,8 @@ public class UserController extends Controller {
// post-login paths // post-login paths
var session = optSession.get(); var session = optSession.get();
sessions.extend(session);
switch (path) { switch (path) {
case "/": case "/":
return sendUserAndCookie(ex, session); return sendUserAndCookie(ex, session);
@ -121,28 +127,26 @@ public class UserController extends Controller {
return notFound(ex); return notFound(ex);
} }
private boolean resetPassword(HttpExchange ex) throws IOException {
var data = json(ex); private boolean generateResetLink(HttpExchange ex) throws IOException {
if (!data.has(TOKEN)) return sendContent(ex, HTTP_UNAUTHORIZED, "token missing"); var idOrEmail = queryParam(ex).get("user");
var newpass = data.getJSONArray("newpass"); var url = url(ex) //
var newPass1 = newpass.getString(0); .replace("/api/user/", "/web/")
if (!newPass1.equals(newpass.getString(1))) { .split("\\?")[0] +
return badRequest(ex, "password mismatch"); ".html";
Set<User> 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"); return sendEmptyResponse(HTTP_OK, ex);
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, "/");
} }
private boolean strong(String pass) {
return pass.length() > 10; // TODO
}
private boolean list(HttpExchange ex, Session session) throws IOException { private boolean list(HttpExchange ex, Session session) throws IOException {
var user = session.user(); var user = session.user();
@ -169,25 +173,31 @@ public class UserController extends Controller {
return sendEmptyResponse(HTTP_OK, ex); return sendEmptyResponse(HTTP_OK, ex);
} }
private boolean generateResetLink(HttpExchange ex) throws IOException {
var idOrEmail = queryParam(ex).get("user"); private boolean resetPassword(HttpExchange ex) throws IOException {
var url = url(ex) // var data = json(ex);
.replace("/api/user/", "/web/") if (!data.has(TOKEN)) return sendContent(ex, HTTP_UNAUTHORIZED, "token missing");
.split("\\?")[0] + var passwords = data.getJSONArray("newpass");
".html"; var newPass = passwords.getString(0);
Set<User> matchingUsers = users.find(idOrEmail); if (!newPass.equals(passwords.getString(1))) {
if (!matchingUsers.isEmpty()) { return badRequest(ex, "password mismatch");
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));
});
} }
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) { private void sendResetLink(User user, String template, String url) {
LOG.log(WARNING, "Sending password link to {0}", user.email()); LOG.log(WARNING, "Sending password link to {0}", user.email());
var token = users.accessToken(user); var token = users.accessToken(user);
@ -228,6 +238,30 @@ public class UserController extends Controller {
return sendContent(ex, session.user().map(false)); 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 { private boolean updatePassword(HttpExchange ex, Session session) throws IOException {
var user = session.user(); var user = session.user();
var json = json(ex); var json = json(ex);
@ -238,12 +272,17 @@ public class UserController extends Controller {
var oldPass = json.getString("oldpass"); var oldPass = json.getString("oldpass");
if (!users.passwordMatches(oldPass, user.hashedPassword())) return badRequest(ex, "wrong password"); if (!users.passwordMatches(oldPass, user.hashedPassword())) return badRequest(ex, "wrong password");
var newpass = json.getJSONArray("newpass"); var passwords = json.getJSONArray("newpass");
var newPass1 = newpass.getString(0); var newPass = passwords.getString(0);
if (!newPass1.equals(newpass.getString(1))) { if (!newPass.equals(passwords.getString(1))) {
return badRequest(ex, "password mismatch"); 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)); return sendContent(ex, user.map(false));
} }

6
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 @Override
public Session extend(String sessionId) { public Session extend(Session session) {
return null; var user = session.user();
var endOfSession = Instant.now().plus(user.sessionDuration());
return save(new Session(user, endOfSession, session.id()));
} }
private JSONObject sessions() { private JSONObject sessions() {

4
de.srsoftware.oidc.web/src/main/resources/en/todo.html

@ -12,12 +12,8 @@
<div id="content"> <div id="content">
<h1>to do…</h1> <h1>to do…</h1>
<ul> <ul>
<li>at_hash in ID Token</li>
<li>Session bei Aktivität verlängern</li>
<li>implement token refresh</li> <li>implement token refresh</li>
<li>handle https correctly in PathHandler.hostname</li>
<li>Verschlüsselung im config-File</li> <li>Verschlüsselung im config-File</li>
<li>bessere Implementierung für UserController.strong(pass), anwendung überall da wo passworte geändert werden können</li>
</ul> </ul>
</div> </div>
</body> </body>

Loading…
Cancel
Save