diff --git a/de.srsoftware.oidc.api/build.gradle b/de.srsoftware.oidc.api/build.gradle index 7c9aa20..c56e75d 100644 --- a/de.srsoftware.oidc.api/build.gradle +++ b/de.srsoftware.oidc.api/build.gradle @@ -13,6 +13,7 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter' implementation 'org.json:json:20240303' implementation 'org.bitbucket.b_c:jose4j:0.9.6' + implementation 'com.sun.mail:jakarta.mail:2.0.1' implementation project(':de.srsoftware.utils') } diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Constants.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Constants.java index 46a735c..9f06884 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Constants.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Constants.java @@ -37,7 +37,8 @@ public class Constants { public static final String RESPONSE_TYPE = "response_type"; public static final String SCOPE = "scope"; public static final String SECRET = "secret"; - public static final String SENDER_ADDRESS = "sender_address"; + public static final String SMTP_USER = "smtp_user"; + public static final String SMTP_PASSWORD = "smtp_pass"; public static final String SMTP_AUTH = "smtp_auth"; public static final String SMTP_HOST = "smtp_host"; public static final String SMTP_PORT = "smtp_port"; diff --git a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/MailConfig.java b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/MailConfig.java index 8a9c673..27e9a37 100644 --- a/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/MailConfig.java +++ b/de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/MailConfig.java @@ -3,6 +3,7 @@ package de.srsoftware.oidc.api; import static de.srsoftware.oidc.api.Constants.*; +import jakarta.mail.Authenticator; import java.util.Map; import java.util.Properties; @@ -35,15 +36,20 @@ public interface MailConfig { props.put("mail.smtp.port", smtpPort()); props.put("mail.smtp.auth", smtpAuth() ? "true" : "false"); props.put("mail.smtp.starttls.enable", startTls() ? "true" : "false"); + props.put("mail.smtp.ssl.trust", smtpHost()); return props; } default Map map() { - return Map.of( // - SMTP_HOST, smtpHost(), // - SMTP_PORT, smtpPort(), // - SMTP_AUTH, smtpAuth(), // - SENDER_ADDRESS, senderAddress(), // + return Map.of( // + SMTP_HOST, smtpHost(), // + SMTP_PORT, smtpPort(), // + SMTP_AUTH, smtpAuth(), // + SMTP_USER, senderAddress(), // START_TLS, startTls()); } + + Authenticator authenticator(); + + MailConfig save(); } 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 059c90a..607985e 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 @@ -1,6 +1,7 @@ /* © SRSoftware 2024 */ package de.srsoftware.oidc.backend; +import static de.srsoftware.oidc.api.Constants.*; import static de.srsoftware.oidc.api.data.Permission.MANAGE_SMTP; import static java.net.HttpURLConnection.HTTP_FORBIDDEN; import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; @@ -31,8 +32,33 @@ public class EmailController extends Controller { return notFound(ex); } + @Override + 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(); + switch (path) { + case "/settings": + return saveSettings(ex, user); + } + return notFound(ex); + } + private boolean provideSettings(HttpExchange ex, User user) throws IOException { if (!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); + 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)); + if (data.has(SMTP_USER)) mailConfig.senderAddress(data.getString(SMTP_USER)); + if (data.has(SMTP_PASSWORD)) mailConfig.senderPassword(data.getString(SMTP_PASSWORD)); + if (data.has(SMTP_AUTH)) mailConfig.smtpAuth(data.getBoolean(SMTP_AUTH)); + if (data.has(START_TLS)) mailConfig.startTls(data.getBoolean(START_TLS)); + mailConfig.save(); + return sendContent(ex, "saved"); + } } 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 10b0844..53e8783 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 @@ -20,20 +20,13 @@ import java.util.Optional; import org.json.JSONObject; public class UserController extends Controller { - private final UserService users; - private final MailConfig mailConfig; - private final Authenticator auth; + private final UserService users; + private final MailConfig mailConfig; public UserController(MailConfig mailConfig, SessionService sessionService, UserService userService) { super(sessionService); users = userService; this.mailConfig = mailConfig; - auth = new Authenticator() { - // override the getPasswordAuthentication method - protected PasswordAuthentication getPasswordAuthentication() { - return new PasswordAuthentication(mailConfig.senderAddress(), mailConfig.senderPassword()); - } - }; } private boolean addUser(HttpExchange ex, Session session) throws IOException { @@ -127,7 +120,7 @@ public class UserController extends Controller { private void senPasswordLink(User user) { LOG.log(WARNING, "Sending password link to {0}", user.email()); try { - var session = jakarta.mail.Session.getDefaultInstance(mailConfig.props(), auth); + var session = jakarta.mail.Session.getDefaultInstance(mailConfig.props(), mailConfig.authenticator()); Message message = new MimeMessage(session); message.setFrom(new InternetAddress(mailConfig.senderAddress())); message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(user.email())); diff --git a/de.srsoftware.oidc.datastore.file/build.gradle b/de.srsoftware.oidc.datastore.file/build.gradle index bfa3e4c..6cec6a0 100644 --- a/de.srsoftware.oidc.datastore.file/build.gradle +++ b/de.srsoftware.oidc.datastore.file/build.gradle @@ -16,6 +16,7 @@ dependencies { implementation project(':de.srsoftware.utils') implementation 'org.json:json:20240303' implementation 'org.bitbucket.b_c:jose4j:0.9.6' + implementation 'com.sun.mail:jakarta.mail:2.0.1' } 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 94149a6..0cd64a4 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 @@ -9,6 +9,8 @@ import static java.util.Optional.empty; import de.srsoftware.oidc.api.*; import de.srsoftware.oidc.api.data.*; +import jakarta.mail.Authenticator; +import jakarta.mail.PasswordAuthentication; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; @@ -37,6 +39,7 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe private Map clients = new HashMap<>(); private Map accessTokens = new HashMap<>(); private Map authCodes = new HashMap<>(); + private Authenticator auth; public FileStore(File storage, PasswordHasher passwordHasher) throws IOException { this.storageFile = storage.toPath(); @@ -48,9 +51,10 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe Files.writeString(storageFile, "{}"); } json = new JSONObject(Files.readString(storageFile)); + auth = null; // lazy init! } - private FileStore save() { + public FileStore save() { try { Files.writeString(storageFile, json.toString(2)); return this; @@ -319,6 +323,19 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe /*** MailConfig implementation ***/ + @Override + public Authenticator authenticator() { + if (auth == null) { + auth = new Authenticator() { + // override the getPasswordAuthentication method + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(senderAddress(), senderPassword()); + } + }; + } + return auth; + } + private String mailConfig(String key) { var config = json.getJSONObject(MAILCONFIG); if (config.has(key)) return config.getString(key); @@ -328,71 +345,71 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe private FileStore mailConfig(String key, Object newValue) { var config = json.getJSONObject(MAILCONFIG); config.put(key, newValue); + auth = null; return this; } @Override public String smtpHost() { - return mailConfig("smtp_host"); + return mailConfig(SMTP_HOST); } @Override public MailConfig smtpHost(String newValue) { - return mailConfig("smtp_host", newValue); + return mailConfig(SMTP_HOST, newValue); } @Override public int smtpPort() { - try { - return Integer.parseInt(mailConfig("smtp_port")); - } catch (NumberFormatException nfe) { - return 0; - } + var config = json.getJSONObject(MAILCONFIG); + return config.has(SMTP_PORT) ? config.getInt(SMTP_PORT) : 0; } @Override public MailConfig smtpPort(int newValue) { - return mailConfig("smtp_port", newValue); + return mailConfig(SMTP_PORT, newValue); } @Override public String senderAddress() { - return mailConfig("sender_address"); + return mailConfig(SMTP_USER); } @Override public MailConfig senderAddress(String newValue) { - return mailConfig("sender_address", newValue); + return mailConfig(SMTP_USER, newValue); } @Override public String senderPassword() { - return mailConfig("smtp_password"); + return mailConfig(SMTP_PASSWORD); } @Override public MailConfig senderPassword(String newValue) { - return mailConfig("smtp_password", newValue); + return mailConfig(SMTP_PASSWORD, newValue); } @Override public boolean startTls() { - return "true".equals(mailConfig("start_tls")); + var config = json.getJSONObject(MAILCONFIG); + return config.has(START_TLS) ? config.getBoolean(START_TLS) : false; } @Override public MailConfig startTls(boolean newValue) { - return mailConfig("start_tls", newValue); + return mailConfig(START_TLS, newValue); } @Override public boolean smtpAuth() { - return "true".equals(mailConfig("smtp_auth")); + var config = json.getJSONObject(MAILCONFIG); + return config.has(SMTP_AUTH) ? config.getBoolean(SMTP_AUTH) : false; } @Override public MailConfig smtpAuth(boolean newValue) { - return mailConfig("smtp_auth", newValue); + return mailConfig(SMTP_AUTH, newValue); } } diff --git a/de.srsoftware.oidc.web/src/main/resources/en/scripts/common.js b/de.srsoftware.oidc.web/src/main/resources/en/scripts/common.js index 94bdc29..2e9f374 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/scripts/common.js +++ b/de.srsoftware.oidc.web/src/main/resources/en/scripts/common.js @@ -25,6 +25,10 @@ function hide(id){ get(id).style.display = 'none'; } +function isChecked(id){ + return get(id).checked; +} + function login(){ redirect('login.html?return_to='+encodeURIComponent(window.location.href)); } diff --git a/de.srsoftware.oidc.web/src/main/resources/en/scripts/settings.js b/de.srsoftware.oidc.web/src/main/resources/en/scripts/settings.js index 65254e6..e10591e 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/scripts/settings.js +++ b/de.srsoftware.oidc.web/src/main/resources/en/scripts/settings.js @@ -28,6 +28,24 @@ async function handlePasswordResponse(response){ },10000); } +async function handleSmtpResponse(response){ + if (response.ok){ + hide('wrong_password'); + hide('password_mismatch'); + setText('smtpBtn', 'saved.'); + } else { + setText('smtpBtn', 'Update failed!'); + var text = await response.text(); + if (text == 'wrong password') show('wrong_password'); + if (text == 'password mismatch') show('password_mismatch'); + + } + setTimeout(function(){ + enable('smtpBtn'); + setText('smtpBtn','Update'); + },10000); +} + function handleResponse(response){ if (response.ok){ hide('update_error') @@ -36,8 +54,8 @@ function handleResponse(response){ show('update_error'); setText('updateBtn', 'Update failed!'); } - enable('updateBtn'); setTimeout(function(){ + enable('updateBtn'); setText('updateBtn','Update'); },10000); } @@ -49,6 +67,8 @@ async function handleSettings(response){ for (var key in json){ setValue(key,json[key]); } + get('start_tls').checked = json.start_tls; + get('smtp_auth').checked = json.smtp_auth; show('mail_settings'); } else { hide('mail_settings'); @@ -59,6 +79,25 @@ function passKeyDown(ev){ if (event.keyCode == 13) updatePass(); } +function updateSmtp(){ + disable('smtpBtn'); + var newData = { + smtp_host : getValue('smtp_host'), + smtp_port : getValue('smtp_port'), + smtp_user : getValue('smtp_user'), + smtp_pass : getValue('smtp_pass'), + smtp_auth : isChecked('smtp_auth'), + start_tls : isChecked('start_tls') + } + fetch("/api/email/settings",{ + method : 'POST', + headers : { + 'Content-Type': 'application/json' + }, + body : JSON.stringify(newData) + }).then(handleSmtpResponse); + setText('smtpBtn','sent…'); +} function updatePass(){ diff --git a/de.srsoftware.oidc.web/src/main/resources/en/settings.html b/de.srsoftware.oidc.web/src/main/resources/en/settings.html index ce91820..4ded70a 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/settings.html +++ b/de.srsoftware.oidc.web/src/main/resources/en/settings.html @@ -78,6 +78,7 @@ +