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>
This commit is contained in:
2024-08-20 00:03:58 +02:00
parent d5ff936710
commit d6007493df
7 changed files with 100 additions and 56 deletions

View File

@@ -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);

View File

@@ -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));

View File

@@ -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<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");
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<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));
});
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));
}